Move wiki related funtions from models to services/wiki (#9355)
* Move wiki related funtions from models to services/wiki
This commit is contained in:
parent
e5d8e2d10c
commit
b9309e52f0
7 changed files with 558 additions and 529 deletions
313
models/wiki.go
313
models/wiki.go
|
@ -5,53 +5,12 @@
|
||||||
package models
|
package models
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/log"
|
|
||||||
"code.gitea.io/gitea/modules/sync"
|
|
||||||
|
|
||||||
"github.com/unknwon/com"
|
"github.com/unknwon/com"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
|
|
||||||
wikiWorkingPool = sync.NewExclusivePool()
|
|
||||||
)
|
|
||||||
|
|
||||||
// NormalizeWikiName normalizes a wiki name
|
|
||||||
func NormalizeWikiName(name string) string {
|
|
||||||
return strings.Replace(name, "-", " ", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// WikiNameToSubURL converts a wiki name to its corresponding sub-URL.
|
|
||||||
func WikiNameToSubURL(name string) string {
|
|
||||||
return url.QueryEscape(strings.Replace(name, " ", "-", -1))
|
|
||||||
}
|
|
||||||
|
|
||||||
// WikiNameToFilename converts a wiki name to its corresponding filename.
|
|
||||||
func WikiNameToFilename(name string) string {
|
|
||||||
name = strings.Replace(name, " ", "-", -1)
|
|
||||||
return url.QueryEscape(name) + ".md"
|
|
||||||
}
|
|
||||||
|
|
||||||
// WikiFilenameToName converts a wiki filename to its corresponding page name.
|
|
||||||
func WikiFilenameToName(filename string) (string, error) {
|
|
||||||
if !strings.HasSuffix(filename, ".md") {
|
|
||||||
return "", ErrWikiInvalidFileName{filename}
|
|
||||||
}
|
|
||||||
basename := filename[:len(filename)-3]
|
|
||||||
unescaped, err := url.QueryUnescape(basename)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return NormalizeWikiName(unescaped), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// WikiCloneLink returns clone URLs of repository wiki.
|
// WikiCloneLink returns clone URLs of repository wiki.
|
||||||
func (repo *Repository) WikiCloneLink() *CloneLink {
|
func (repo *Repository) WikiCloneLink() *CloneLink {
|
||||||
return repo.cloneLink(x, true)
|
return repo.cloneLink(x, true)
|
||||||
|
@ -71,275 +30,3 @@ func (repo *Repository) WikiPath() string {
|
||||||
func (repo *Repository) HasWiki() bool {
|
func (repo *Repository) HasWiki() bool {
|
||||||
return com.IsDir(repo.WikiPath())
|
return com.IsDir(repo.WikiPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
// InitWiki initializes a wiki for repository,
|
|
||||||
// it does nothing when repository already has wiki.
|
|
||||||
func (repo *Repository) InitWiki() error {
|
|
||||||
if repo.HasWiki() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.InitRepository(repo.WikiPath(), true); err != nil {
|
|
||||||
return fmt.Errorf("InitRepository: %v", err)
|
|
||||||
} else if err = createDelegateHooks(repo.WikiPath()); err != nil {
|
|
||||||
return fmt.Errorf("createDelegateHooks: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// nameAllowed checks if a wiki name is allowed
|
|
||||||
func nameAllowed(name string) error {
|
|
||||||
for _, reservedName := range reservedWikiNames {
|
|
||||||
if name == reservedName {
|
|
||||||
return ErrWikiReservedName{name}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateWikiPage adds a new page to the repository wiki.
|
|
||||||
func (repo *Repository) updateWikiPage(doer *User, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
|
|
||||||
if err = nameAllowed(newWikiName); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
|
|
||||||
defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
|
|
||||||
|
|
||||||
if err = repo.InitWiki(); err != nil {
|
|
||||||
return fmt.Errorf("InitWiki: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hasMasterBranch := git.IsBranchExist(repo.WikiPath(), "master")
|
|
||||||
|
|
||||||
basePath, err := CreateTemporaryPath("update-wiki")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := RemoveTemporaryPath(basePath); err != nil {
|
|
||||||
log.Error("Merge: RemoveTemporaryPath: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
cloneOpts := git.CloneRepoOptions{
|
|
||||||
Bare: true,
|
|
||||||
Shared: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasMasterBranch {
|
|
||||||
cloneOpts.Branch = "master"
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.Clone(repo.WikiPath(), basePath, cloneOpts); err != nil {
|
|
||||||
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
|
||||||
return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(basePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
|
||||||
return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
|
|
||||||
}
|
|
||||||
defer gitRepo.Close()
|
|
||||||
|
|
||||||
if hasMasterBranch {
|
|
||||||
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
|
||||||
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
|
||||||
return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
newWikiPath := WikiNameToFilename(newWikiName)
|
|
||||||
if isNew {
|
|
||||||
filesInIndex, err := gitRepo.LsFiles(newWikiPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, file := range filesInIndex {
|
|
||||||
if file == newWikiPath {
|
|
||||||
return ErrWikiAlreadyExist{newWikiPath}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
oldWikiPath := WikiNameToFilename(oldWikiName)
|
|
||||||
filesInIndex, err := gitRepo.LsFiles(oldWikiPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, file := range filesInIndex {
|
|
||||||
if file == oldWikiPath {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
|
||||||
|
|
||||||
objectHash, err := gitRepo.HashObject(strings.NewReader(content))
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tree, err := gitRepo.WriteTree()
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
commitTreeOpts := git.CommitTreeOpts{
|
|
||||||
Message: message,
|
|
||||||
}
|
|
||||||
|
|
||||||
sign, signingKey := repo.SignWikiCommit(doer)
|
|
||||||
if sign {
|
|
||||||
commitTreeOpts.KeyID = signingKey
|
|
||||||
} else {
|
|
||||||
commitTreeOpts.NoGPGSign = true
|
|
||||||
}
|
|
||||||
if hasMasterBranch {
|
|
||||||
commitTreeOpts.Parents = []string{"HEAD"}
|
|
||||||
}
|
|
||||||
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.Push(basePath, git.PushOptions{
|
|
||||||
Remote: "origin",
|
|
||||||
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
|
|
||||||
Env: FullPushingEnvironment(
|
|
||||||
doer,
|
|
||||||
doer,
|
|
||||||
repo,
|
|
||||||
repo.Name+".wiki",
|
|
||||||
0,
|
|
||||||
),
|
|
||||||
}); err != nil {
|
|
||||||
log.Error("%v", err)
|
|
||||||
return fmt.Errorf("Push: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddWikiPage adds a new wiki page with a given wikiPath.
|
|
||||||
func (repo *Repository) AddWikiPage(doer *User, wikiName, content, message string) error {
|
|
||||||
return repo.updateWikiPage(doer, "", wikiName, content, message, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EditWikiPage updates a wiki page identified by its wikiPath,
|
|
||||||
// optionally also changing wikiPath.
|
|
||||||
func (repo *Repository) EditWikiPage(doer *User, oldWikiName, newWikiName, content, message string) error {
|
|
||||||
return repo.updateWikiPage(doer, oldWikiName, newWikiName, content, message, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteWikiPage deletes a wiki page identified by its path.
|
|
||||||
func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) {
|
|
||||||
wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
|
|
||||||
defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
|
|
||||||
|
|
||||||
if err = repo.InitWiki(); err != nil {
|
|
||||||
return fmt.Errorf("InitWiki: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
basePath, err := CreateTemporaryPath("update-wiki")
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
if err := RemoveTemporaryPath(basePath); err != nil {
|
|
||||||
log.Error("Merge: RemoveTemporaryPath: %s", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
|
|
||||||
Bare: true,
|
|
||||||
Shared: true,
|
|
||||||
Branch: "master",
|
|
||||||
}); err != nil {
|
|
||||||
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
|
||||||
return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gitRepo, err := git.OpenRepository(basePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
|
||||||
return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
|
|
||||||
}
|
|
||||||
defer gitRepo.Close()
|
|
||||||
|
|
||||||
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
|
||||||
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
|
||||||
return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
wikiPath := WikiNameToFilename(wikiName)
|
|
||||||
filesInIndex, err := gitRepo.LsFiles(wikiPath)
|
|
||||||
found := false
|
|
||||||
for _, file := range filesInIndex {
|
|
||||||
if file == wikiPath {
|
|
||||||
found = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if found {
|
|
||||||
err := gitRepo.RemoveFilesFromIndex(wikiPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
|
||||||
|
|
||||||
tree, err := gitRepo.WriteTree()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
message := "Delete page '" + wikiName + "'"
|
|
||||||
commitTreeOpts := git.CommitTreeOpts{
|
|
||||||
Message: message,
|
|
||||||
Parents: []string{"HEAD"},
|
|
||||||
}
|
|
||||||
|
|
||||||
sign, signingKey := repo.SignWikiCommit(doer)
|
|
||||||
if sign {
|
|
||||||
commitTreeOpts.KeyID = signingKey
|
|
||||||
} else {
|
|
||||||
commitTreeOpts.NoGPGSign = true
|
|
||||||
}
|
|
||||||
|
|
||||||
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := git.Push(basePath, git.PushOptions{
|
|
||||||
Remote: "origin",
|
|
||||||
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
|
|
||||||
Env: PushingEnvironment(doer, repo),
|
|
||||||
}); err != nil {
|
|
||||||
return fmt.Errorf("Push: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -8,100 +8,11 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNormalizeWikiName(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
Expected string
|
|
||||||
WikiName string
|
|
||||||
}
|
|
||||||
for _, test := range []test{
|
|
||||||
{"wiki name", "wiki name"},
|
|
||||||
{"wiki name", "wiki-name"},
|
|
||||||
{"name with/slash", "name with/slash"},
|
|
||||||
{"name with%percent", "name-with%percent"},
|
|
||||||
{"%2F", "%2F"},
|
|
||||||
} {
|
|
||||||
assert.Equal(t, test.Expected, NormalizeWikiName(test.WikiName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWikiNameToFilename(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
Expected string
|
|
||||||
WikiName string
|
|
||||||
}
|
|
||||||
for _, test := range []test{
|
|
||||||
{"wiki-name.md", "wiki name"},
|
|
||||||
{"wiki-name.md", "wiki-name"},
|
|
||||||
{"name-with%2Fslash.md", "name with/slash"},
|
|
||||||
{"name-with%25percent.md", "name with%percent"},
|
|
||||||
} {
|
|
||||||
assert.Equal(t, test.Expected, WikiNameToFilename(test.WikiName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWikiNameToSubURL(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
Expected string
|
|
||||||
WikiName string
|
|
||||||
}
|
|
||||||
for _, test := range []test{
|
|
||||||
{"wiki-name", "wiki name"},
|
|
||||||
{"wiki-name", "wiki-name"},
|
|
||||||
{"name-with%2Fslash", "name with/slash"},
|
|
||||||
{"name-with%25percent", "name with%percent"},
|
|
||||||
} {
|
|
||||||
assert.Equal(t, test.Expected, WikiNameToSubURL(test.WikiName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWikiFilenameToName(t *testing.T) {
|
|
||||||
type test struct {
|
|
||||||
Expected string
|
|
||||||
Filename string
|
|
||||||
}
|
|
||||||
for _, test := range []test{
|
|
||||||
{"hello world", "hello-world.md"},
|
|
||||||
{"symbols/?*", "symbols%2F%3F%2A.md"},
|
|
||||||
} {
|
|
||||||
name, err := WikiFilenameToName(test.Filename)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, test.Expected, name)
|
|
||||||
}
|
|
||||||
for _, badFilename := range []string{
|
|
||||||
"nofileextension",
|
|
||||||
"wrongfileextension.txt",
|
|
||||||
} {
|
|
||||||
_, err := WikiFilenameToName(badFilename)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, IsErrWikiInvalidFileName(err))
|
|
||||||
}
|
|
||||||
_, err := WikiFilenameToName("badescaping%%.md")
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.False(t, IsErrWikiInvalidFileName(err))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWikiNameToFilenameToName(t *testing.T) {
|
|
||||||
// converting from wiki name to filename, then back to wiki name should
|
|
||||||
// return the original (normalized) name
|
|
||||||
for _, name := range []string{
|
|
||||||
"wiki-name",
|
|
||||||
"wiki name",
|
|
||||||
"wiki name with/slash",
|
|
||||||
"$$$%%%^^&&!@#$(),.<>",
|
|
||||||
} {
|
|
||||||
filename := WikiNameToFilename(name)
|
|
||||||
resultName, err := WikiFilenameToName(filename)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, NormalizeWikiName(name), resultName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepository_WikiCloneLink(t *testing.T) {
|
func TestRepository_WikiCloneLink(t *testing.T) {
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
assert.NoError(t, PrepareTestDatabase())
|
||||||
|
|
||||||
|
@ -131,107 +42,3 @@ func TestRepository_HasWiki(t *testing.T) {
|
||||||
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
||||||
assert.False(t, repo2.HasWiki())
|
assert.False(t, repo2.HasWiki())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepository_InitWiki(t *testing.T) {
|
|
||||||
PrepareTestEnv(t)
|
|
||||||
// repo1 already has a wiki
|
|
||||||
repo1 := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
|
||||||
assert.NoError(t, repo1.InitWiki())
|
|
||||||
|
|
||||||
// repo2 does not already have a wiki
|
|
||||||
repo2 := AssertExistsAndLoadBean(t, &Repository{ID: 2}).(*Repository)
|
|
||||||
assert.NoError(t, repo2.InitWiki())
|
|
||||||
assert.True(t, repo2.HasWiki())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepository_AddWikiPage(t *testing.T) {
|
|
||||||
assert.NoError(t, PrepareTestDatabase())
|
|
||||||
const wikiContent = "This is the wiki content"
|
|
||||||
const commitMsg = "Commit message"
|
|
||||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
|
||||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
|
||||||
for _, wikiName := range []string{
|
|
||||||
"Another page",
|
|
||||||
"Here's a <tag> and a/slash",
|
|
||||||
} {
|
|
||||||
wikiName := wikiName
|
|
||||||
t.Run("test wiki exist: "+wikiName, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
assert.NoError(t, repo.AddWikiPage(doer, wikiName, wikiContent, commitMsg))
|
|
||||||
// Now need to show that the page has been added:
|
|
||||||
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer gitRepo.Close()
|
|
||||||
masterTree, err := gitRepo.GetTree("master")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
wikiPath := WikiNameToFilename(wikiName)
|
|
||||||
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, wikiPath, entry.Name(), "%s not addded correctly", wikiName)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Run("check wiki already exist", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// test for already-existing wiki name
|
|
||||||
err := repo.AddWikiPage(doer, "Home", wikiContent, commitMsg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, IsErrWikiAlreadyExist(err))
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("check wiki reserved name", func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
// test for reserved wiki name
|
|
||||||
err := repo.AddWikiPage(doer, "_edit", wikiContent, commitMsg)
|
|
||||||
assert.Error(t, err)
|
|
||||||
assert.True(t, IsErrWikiReservedName(err))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepository_EditWikiPage(t *testing.T) {
|
|
||||||
const newWikiContent = "This is the new content"
|
|
||||||
const commitMsg = "Commit message"
|
|
||||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
|
||||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
|
||||||
for _, newWikiName := range []string{
|
|
||||||
"Home", // same name as before
|
|
||||||
"New home",
|
|
||||||
"New/name/with/slashes",
|
|
||||||
} {
|
|
||||||
PrepareTestEnv(t)
|
|
||||||
assert.NoError(t, repo.EditWikiPage(doer, "Home", newWikiName, newWikiContent, commitMsg))
|
|
||||||
|
|
||||||
// Now need to show that the page has been added:
|
|
||||||
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
masterTree, err := gitRepo.GetTree("master")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
wikiPath := WikiNameToFilename(newWikiName)
|
|
||||||
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, wikiPath, entry.Name(), "%s not editted correctly", newWikiName)
|
|
||||||
|
|
||||||
if newWikiName != "Home" {
|
|
||||||
_, err := masterTree.GetTreeEntryByPath("Home.md")
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
gitRepo.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepository_DeleteWikiPage(t *testing.T) {
|
|
||||||
PrepareTestEnv(t)
|
|
||||||
repo := AssertExistsAndLoadBean(t, &Repository{ID: 1}).(*Repository)
|
|
||||||
doer := AssertExistsAndLoadBean(t, &User{ID: 2}).(*User)
|
|
||||||
assert.NoError(t, repo.DeleteWikiPage(doer, "Home"))
|
|
||||||
|
|
||||||
// Now need to show that the page has been added:
|
|
||||||
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer gitRepo.Close()
|
|
||||||
masterTree, err := gitRepo.GetTree("master")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
wikiPath := WikiNameToFilename("Home")
|
|
||||||
_, err = masterTree.GetTreeEntryByPath(wikiPath)
|
|
||||||
assert.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/private"
|
"code.gitea.io/gitea/modules/private"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
|
wiki_service "code.gitea.io/gitea/services/wiki"
|
||||||
|
|
||||||
"gitea.com/macaron/macaron"
|
"gitea.com/macaron/macaron"
|
||||||
)
|
)
|
||||||
|
@ -320,7 +321,7 @@ func ServCommand(ctx *macaron.Context) {
|
||||||
|
|
||||||
// Finally if we're trying to touch the wiki we should init it
|
// Finally if we're trying to touch the wiki we should init it
|
||||||
if results.IsWiki {
|
if results.IsWiki {
|
||||||
if err = repo.InitWiki(); err != nil {
|
if err = wiki_service.InitWiki(repo); err != nil {
|
||||||
log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err)
|
log.Error("Failed to initialize the wiki in %-v Error: %v", repo, err)
|
||||||
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
ctx.JSON(http.StatusInternalServerError, map[string]interface{}{
|
||||||
"results": results,
|
"results": results,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/markup/markdown"
|
"code.gitea.io/gitea/modules/markup/markdown"
|
||||||
"code.gitea.io/gitea/modules/timeutil"
|
"code.gitea.io/gitea/modules/timeutil"
|
||||||
"code.gitea.io/gitea/modules/util"
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
wiki_service "code.gitea.io/gitea/services/wiki"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -124,7 +125,7 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
|
||||||
func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
|
func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) {
|
||||||
var entry *git.TreeEntry
|
var entry *git.TreeEntry
|
||||||
var err error
|
var err error
|
||||||
pageFilename := models.WikiNameToFilename(wikiName)
|
pageFilename := wiki_service.NameToFilename(wikiName)
|
||||||
if entry, err = findEntryForFile(commit, pageFilename); err != nil {
|
if entry, err = findEntryForFile(commit, pageFilename); err != nil {
|
||||||
ctx.ServerError("findEntryForFile", err)
|
ctx.ServerError("findEntryForFile", err)
|
||||||
return nil, nil, "", false
|
return nil, nil, "", false
|
||||||
|
@ -157,7 +158,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
if !entry.IsRegular() {
|
if !entry.IsRegular() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wikiName, err := models.WikiFilenameToName(entry.Name())
|
wikiName, err := wiki_service.FilenameToName(entry.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrWikiInvalidFileName(err) {
|
if models.IsErrWikiInvalidFileName(err) {
|
||||||
continue
|
continue
|
||||||
|
@ -172,17 +173,17 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
|
||||||
}
|
}
|
||||||
pages = append(pages, PageMeta{
|
pages = append(pages, PageMeta{
|
||||||
Name: wikiName,
|
Name: wikiName,
|
||||||
SubURL: models.WikiNameToSubURL(wikiName),
|
SubURL: wiki_service.NameToSubURL(wikiName),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
ctx.Data["Pages"] = pages
|
ctx.Data["Pages"] = pages
|
||||||
|
|
||||||
// get requested pagename
|
// get requested pagename
|
||||||
pageName := models.NormalizeWikiName(ctx.Params(":page"))
|
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
|
||||||
if len(pageName) == 0 {
|
if len(pageName) == 0 {
|
||||||
pageName = "Home"
|
pageName = "Home"
|
||||||
}
|
}
|
||||||
ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
|
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
|
||||||
ctx.Data["old_title"] = pageName
|
ctx.Data["old_title"] = pageName
|
||||||
ctx.Data["Title"] = pageName
|
ctx.Data["Title"] = pageName
|
||||||
ctx.Data["title"] = pageName
|
ctx.Data["title"] = pageName
|
||||||
|
@ -243,11 +244,11 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get requested pagename
|
// get requested pagename
|
||||||
pageName := models.NormalizeWikiName(ctx.Params(":page"))
|
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
|
||||||
if len(pageName) == 0 {
|
if len(pageName) == 0 {
|
||||||
pageName = "Home"
|
pageName = "Home"
|
||||||
}
|
}
|
||||||
ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
|
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
|
||||||
ctx.Data["old_title"] = pageName
|
ctx.Data["old_title"] = pageName
|
||||||
ctx.Data["Title"] = pageName
|
ctx.Data["Title"] = pageName
|
||||||
ctx.Data["title"] = pageName
|
ctx.Data["title"] = pageName
|
||||||
|
@ -320,11 +321,11 @@ func renderEditPage(ctx *context.Context) {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// get requested pagename
|
// get requested pagename
|
||||||
pageName := models.NormalizeWikiName(ctx.Params(":page"))
|
pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
|
||||||
if len(pageName) == 0 {
|
if len(pageName) == 0 {
|
||||||
pageName = "Home"
|
pageName = "Home"
|
||||||
}
|
}
|
||||||
ctx.Data["PageURL"] = models.WikiNameToSubURL(pageName)
|
ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName)
|
||||||
ctx.Data["old_title"] = pageName
|
ctx.Data["old_title"] = pageName
|
||||||
ctx.Data["Title"] = pageName
|
ctx.Data["Title"] = pageName
|
||||||
ctx.Data["title"] = pageName
|
ctx.Data["title"] = pageName
|
||||||
|
@ -474,7 +475,7 @@ func WikiPages(ctx *context.Context) {
|
||||||
ctx.ServerError("GetCommit", err)
|
ctx.ServerError("GetCommit", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
wikiName, err := models.WikiFilenameToName(entry.Name())
|
wikiName, err := wiki_service.FilenameToName(entry.Name())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if models.IsErrWikiInvalidFileName(err) {
|
if models.IsErrWikiInvalidFileName(err) {
|
||||||
continue
|
continue
|
||||||
|
@ -488,7 +489,7 @@ func WikiPages(ctx *context.Context) {
|
||||||
}
|
}
|
||||||
pages = append(pages, PageMeta{
|
pages = append(pages, PageMeta{
|
||||||
Name: wikiName,
|
Name: wikiName,
|
||||||
SubURL: models.WikiNameToSubURL(wikiName),
|
SubURL: wiki_service.NameToSubURL(wikiName),
|
||||||
UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
|
UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -528,7 +529,7 @@ func WikiRaw(ctx *context.Context) {
|
||||||
providedPath = providedPath[:len(providedPath)-3]
|
providedPath = providedPath[:len(providedPath)-3]
|
||||||
}
|
}
|
||||||
|
|
||||||
wikiPath := models.WikiNameToFilename(providedPath)
|
wikiPath := wiki_service.NameToFilename(providedPath)
|
||||||
entry, err = findEntryForFile(commit, wikiPath)
|
entry, err = findEntryForFile(commit, wikiPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("findFile", err)
|
ctx.ServerError("findFile", err)
|
||||||
|
@ -576,8 +577,8 @@ func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
wikiName := models.NormalizeWikiName(form.Title)
|
wikiName := wiki_service.NormalizeWikiName(form.Title)
|
||||||
if err := ctx.Repo.Repository.AddWikiPage(ctx.User, wikiName, form.Content, form.Message); err != nil {
|
if err := wiki_service.AddWikiPage(ctx.User, ctx.Repo.Repository, wikiName, form.Content, form.Message); err != nil {
|
||||||
if models.IsErrWikiReservedName(err) {
|
if models.IsErrWikiReservedName(err) {
|
||||||
ctx.Data["Err_Title"] = true
|
ctx.Data["Err_Title"] = true
|
||||||
ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
|
ctx.RenderWithErr(ctx.Tr("repo.wiki.reserved_page", wikiName), tplWikiNew, &form)
|
||||||
|
@ -590,7 +591,7 @@ func NewWikiPost(ctx *context.Context, form auth.NewWikiForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(wikiName))
|
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditWiki render wiki modify page
|
// EditWiki render wiki modify page
|
||||||
|
@ -623,25 +624,25 @@ func EditWikiPost(ctx *context.Context, form auth.NewWikiForm) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oldWikiName := models.NormalizeWikiName(ctx.Params(":page"))
|
oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
|
||||||
newWikiName := models.NormalizeWikiName(form.Title)
|
newWikiName := wiki_service.NormalizeWikiName(form.Title)
|
||||||
|
|
||||||
if err := ctx.Repo.Repository.EditWikiPage(ctx.User, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
|
if err := wiki_service.EditWikiPage(ctx.User, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.Message); err != nil {
|
||||||
ctx.ServerError("EditWikiPage", err)
|
ctx.ServerError("EditWikiPage", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + models.WikiNameToSubURL(newWikiName))
|
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteWikiPagePost delete wiki page
|
// DeleteWikiPagePost delete wiki page
|
||||||
func DeleteWikiPagePost(ctx *context.Context) {
|
func DeleteWikiPagePost(ctx *context.Context) {
|
||||||
wikiName := models.NormalizeWikiName(ctx.Params(":page"))
|
wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
|
||||||
if len(wikiName) == 0 {
|
if len(wikiName) == 0 {
|
||||||
wikiName = "Home"
|
wikiName = "Home"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ctx.Repo.Repository.DeleteWikiPage(ctx.User, wikiName); err != nil {
|
if err := wiki_service.DeleteWikiPage(ctx.User, ctx.Repo.Repository, wikiName); err != nil {
|
||||||
ctx.ServerError("DeleteWikiPage", err)
|
ctx.ServerError("DeleteWikiPage", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
"code.gitea.io/gitea/modules/auth"
|
"code.gitea.io/gitea/modules/auth"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/test"
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
wiki_service "code.gitea.io/gitea/services/wiki"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
@ -29,7 +30,7 @@ func wikiEntry(t *testing.T, repo *models.Repository, wikiName string) *git.Tree
|
||||||
entries, err := commit.ListEntries()
|
entries, err := commit.ListEntries()
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if entry.Name() == models.WikiNameToFilename(wikiName) {
|
if entry.Name() == wiki_service.NameToFilename(wikiName) {
|
||||||
return entry
|
return entry
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
322
services/wiki/wiki.go
Normal file
322
services/wiki/wiki.go
Normal file
|
@ -0,0 +1,322 @@
|
||||||
|
// Copyright 2015 The Gogs Authors. All rights reserved.
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package wiki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/log"
|
||||||
|
"code.gitea.io/gitea/modules/sync"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
|
"github.com/unknwon/com"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
|
||||||
|
wikiWorkingPool = sync.NewExclusivePool()
|
||||||
|
)
|
||||||
|
|
||||||
|
func nameAllowed(name string) error {
|
||||||
|
if util.IsStringInSlice(name, reservedWikiNames) {
|
||||||
|
return models.ErrWikiReservedName{
|
||||||
|
Title: name,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameToSubURL converts a wiki name to its corresponding sub-URL.
|
||||||
|
func NameToSubURL(name string) string {
|
||||||
|
return url.QueryEscape(strings.Replace(name, " ", "-", -1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeWikiName normalizes a wiki name
|
||||||
|
func NormalizeWikiName(name string) string {
|
||||||
|
return strings.Replace(name, "-", " ", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NameToFilename converts a wiki name to its corresponding filename.
|
||||||
|
func NameToFilename(name string) string {
|
||||||
|
name = strings.Replace(name, " ", "-", -1)
|
||||||
|
return url.QueryEscape(name) + ".md"
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilenameToName converts a wiki filename to its corresponding page name.
|
||||||
|
func FilenameToName(filename string) (string, error) {
|
||||||
|
if !strings.HasSuffix(filename, ".md") {
|
||||||
|
return "", models.ErrWikiInvalidFileName{
|
||||||
|
FileName: filename,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
basename := filename[:len(filename)-3]
|
||||||
|
unescaped, err := url.QueryUnescape(basename)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return NormalizeWikiName(unescaped), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitWiki initializes a wiki for repository,
|
||||||
|
// it does nothing when repository already has wiki.
|
||||||
|
func InitWiki(repo *models.Repository) error {
|
||||||
|
if repo.HasWiki() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.InitRepository(repo.WikiPath(), true); err != nil {
|
||||||
|
return fmt.Errorf("InitRepository: %v", err)
|
||||||
|
} else if err = models.CreateDelegateHooks(repo.WikiPath()); err != nil {
|
||||||
|
return fmt.Errorf("createDelegateHooks: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateWikiPage adds a new page to the repository wiki.
|
||||||
|
func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
|
||||||
|
if err = nameAllowed(newWikiName); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
|
||||||
|
defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
|
||||||
|
|
||||||
|
if err = InitWiki(repo); err != nil {
|
||||||
|
return fmt.Errorf("InitWiki: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hasMasterBranch := git.IsBranchExist(repo.WikiPath(), "master")
|
||||||
|
|
||||||
|
basePath, err := models.CreateTemporaryPath("update-wiki")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := models.RemoveTemporaryPath(basePath); err != nil {
|
||||||
|
log.Error("Merge: RemoveTemporaryPath: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cloneOpts := git.CloneRepoOptions{
|
||||||
|
Bare: true,
|
||||||
|
Shared: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasMasterBranch {
|
||||||
|
cloneOpts.Branch = "master"
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.Clone(repo.WikiPath(), basePath, cloneOpts); err != nil {
|
||||||
|
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||||
|
return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(basePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
||||||
|
return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
if hasMasterBranch {
|
||||||
|
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
||||||
|
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||||
|
return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newWikiPath := NameToFilename(newWikiName)
|
||||||
|
if isNew {
|
||||||
|
filesInIndex, err := gitRepo.LsFiles(newWikiPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if util.IsStringInSlice(newWikiPath, filesInIndex) {
|
||||||
|
return models.ErrWikiAlreadyExist{
|
||||||
|
Title: newWikiPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
oldWikiPath := NameToFilename(oldWikiName)
|
||||||
|
filesInIndex, err := gitRepo.LsFiles(oldWikiPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.IsStringInSlice(oldWikiPath, filesInIndex) {
|
||||||
|
err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
||||||
|
|
||||||
|
objectHash, err := gitRepo.HashObject(strings.NewReader(content))
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tree, err := gitRepo.WriteTree()
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
commitTreeOpts := git.CommitTreeOpts{
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
|
||||||
|
sign, signingKey := repo.SignWikiCommit(doer)
|
||||||
|
if sign {
|
||||||
|
commitTreeOpts.KeyID = signingKey
|
||||||
|
} else {
|
||||||
|
commitTreeOpts.NoGPGSign = true
|
||||||
|
}
|
||||||
|
if hasMasterBranch {
|
||||||
|
commitTreeOpts.Parents = []string{"HEAD"}
|
||||||
|
}
|
||||||
|
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.Push(basePath, git.PushOptions{
|
||||||
|
Remote: "origin",
|
||||||
|
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
|
||||||
|
Env: models.FullPushingEnvironment(
|
||||||
|
doer,
|
||||||
|
doer,
|
||||||
|
repo,
|
||||||
|
repo.Name+".wiki",
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
}); err != nil {
|
||||||
|
log.Error("%v", err)
|
||||||
|
return fmt.Errorf("Push: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWikiPage adds a new wiki page with a given wikiPath.
|
||||||
|
func AddWikiPage(doer *models.User, repo *models.Repository, wikiName, content, message string) error {
|
||||||
|
return updateWikiPage(doer, repo, "", wikiName, content, message, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EditWikiPage updates a wiki page identified by its wikiPath,
|
||||||
|
// optionally also changing wikiPath.
|
||||||
|
func EditWikiPage(doer *models.User, repo *models.Repository, oldWikiName, newWikiName, content, message string) error {
|
||||||
|
return updateWikiPage(doer, repo, oldWikiName, newWikiName, content, message, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWikiPage deletes a wiki page identified by its path.
|
||||||
|
func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) (err error) {
|
||||||
|
wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
|
||||||
|
defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
|
||||||
|
|
||||||
|
if err = InitWiki(repo); err != nil {
|
||||||
|
return fmt.Errorf("InitWiki: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
basePath, err := models.CreateTemporaryPath("update-wiki")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if err := models.RemoveTemporaryPath(basePath); err != nil {
|
||||||
|
log.Error("Merge: RemoveTemporaryPath: %s", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
|
||||||
|
Bare: true,
|
||||||
|
Shared: true,
|
||||||
|
Branch: "master",
|
||||||
|
}); err != nil {
|
||||||
|
log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||||
|
return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gitRepo, err := git.OpenRepository(basePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
|
||||||
|
return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
|
||||||
|
}
|
||||||
|
defer gitRepo.Close()
|
||||||
|
|
||||||
|
if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
|
||||||
|
log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||||
|
return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wikiPath := NameToFilename(wikiName)
|
||||||
|
filesInIndex, err := gitRepo.LsFiles(wikiPath)
|
||||||
|
found := false
|
||||||
|
for _, file := range filesInIndex {
|
||||||
|
if file == wikiPath {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if found {
|
||||||
|
err := gitRepo.RemoveFilesFromIndex(wikiPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
|
||||||
|
|
||||||
|
tree, err := gitRepo.WriteTree()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
message := "Delete page '" + wikiName + "'"
|
||||||
|
commitTreeOpts := git.CommitTreeOpts{
|
||||||
|
Message: message,
|
||||||
|
Parents: []string{"HEAD"},
|
||||||
|
}
|
||||||
|
|
||||||
|
sign, signingKey := repo.SignWikiCommit(doer)
|
||||||
|
if sign {
|
||||||
|
commitTreeOpts.KeyID = signingKey
|
||||||
|
} else {
|
||||||
|
commitTreeOpts.NoGPGSign = true
|
||||||
|
}
|
||||||
|
|
||||||
|
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := git.Push(basePath, git.PushOptions{
|
||||||
|
Remote: "origin",
|
||||||
|
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
|
||||||
|
Env: models.PushingEnvironment(doer, repo),
|
||||||
|
}); err != nil {
|
||||||
|
return fmt.Errorf("Push: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
210
services/wiki/wiki_test.go
Normal file
210
services/wiki/wiki_test.go
Normal file
|
@ -0,0 +1,210 @@
|
||||||
|
// Copyright 2019 The Gitea Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package wiki
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models"
|
||||||
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
models.MainTest(m, filepath.Join("..", ".."))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiNameToSubURL(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
Expected string
|
||||||
|
WikiName string
|
||||||
|
}
|
||||||
|
for _, test := range []test{
|
||||||
|
{"wiki-name", "wiki name"},
|
||||||
|
{"wiki-name", "wiki-name"},
|
||||||
|
{"name-with%2Fslash", "name with/slash"},
|
||||||
|
{"name-with%25percent", "name with%percent"},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, test.Expected, NameToSubURL(test.WikiName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNormalizeWikiName(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
Expected string
|
||||||
|
WikiName string
|
||||||
|
}
|
||||||
|
for _, test := range []test{
|
||||||
|
{"wiki name", "wiki name"},
|
||||||
|
{"wiki name", "wiki-name"},
|
||||||
|
{"name with/slash", "name with/slash"},
|
||||||
|
{"name with%percent", "name-with%percent"},
|
||||||
|
{"%2F", "%2F"},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, test.Expected, NormalizeWikiName(test.WikiName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiNameToFilename(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
Expected string
|
||||||
|
WikiName string
|
||||||
|
}
|
||||||
|
for _, test := range []test{
|
||||||
|
{"wiki-name.md", "wiki name"},
|
||||||
|
{"wiki-name.md", "wiki-name"},
|
||||||
|
{"name-with%2Fslash.md", "name with/slash"},
|
||||||
|
{"name-with%25percent.md", "name with%percent"},
|
||||||
|
} {
|
||||||
|
assert.Equal(t, test.Expected, NameToFilename(test.WikiName))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiFilenameToName(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
Expected string
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
for _, test := range []test{
|
||||||
|
{"hello world", "hello-world.md"},
|
||||||
|
{"symbols/?*", "symbols%2F%3F%2A.md"},
|
||||||
|
} {
|
||||||
|
name, err := FilenameToName(test.Filename)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, test.Expected, name)
|
||||||
|
}
|
||||||
|
for _, badFilename := range []string{
|
||||||
|
"nofileextension",
|
||||||
|
"wrongfileextension.txt",
|
||||||
|
} {
|
||||||
|
_, err := FilenameToName(badFilename)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, models.IsErrWikiInvalidFileName(err))
|
||||||
|
}
|
||||||
|
_, err := FilenameToName("badescaping%%.md")
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.False(t, models.IsErrWikiInvalidFileName(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiNameToFilenameToName(t *testing.T) {
|
||||||
|
// converting from wiki name to filename, then back to wiki name should
|
||||||
|
// return the original (normalized) name
|
||||||
|
for _, name := range []string{
|
||||||
|
"wiki-name",
|
||||||
|
"wiki name",
|
||||||
|
"wiki name with/slash",
|
||||||
|
"$$$%%%^^&&!@#$(),.<>",
|
||||||
|
} {
|
||||||
|
filename := NameToFilename(name)
|
||||||
|
resultName, err := FilenameToName(filename)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, NormalizeWikiName(name), resultName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepository_InitWiki(t *testing.T) {
|
||||||
|
models.PrepareTestEnv(t)
|
||||||
|
// repo1 already has a wiki
|
||||||
|
repo1 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
assert.NoError(t, InitWiki(repo1))
|
||||||
|
|
||||||
|
// repo2 does not already have a wiki
|
||||||
|
repo2 := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 2}).(*models.Repository)
|
||||||
|
assert.NoError(t, InitWiki(repo2))
|
||||||
|
assert.True(t, repo2.HasWiki())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepository_AddWikiPage(t *testing.T) {
|
||||||
|
assert.NoError(t, models.PrepareTestDatabase())
|
||||||
|
const wikiContent = "This is the wiki content"
|
||||||
|
const commitMsg = "Commit message"
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
for _, wikiName := range []string{
|
||||||
|
"Another page",
|
||||||
|
"Here's a <tag> and a/slash",
|
||||||
|
} {
|
||||||
|
wikiName := wikiName
|
||||||
|
t.Run("test wiki exist: "+wikiName, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
assert.NoError(t, AddWikiPage(doer, repo, wikiName, wikiContent, commitMsg))
|
||||||
|
// Now need to show that the page has been added:
|
||||||
|
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
masterTree, err := gitRepo.GetTree("master")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
wikiPath := NameToFilename(wikiName)
|
||||||
|
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, wikiPath, entry.Name(), "%s not addded correctly", wikiName)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("check wiki already exist", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// test for already-existing wiki name
|
||||||
|
err := AddWikiPage(doer, repo, "Home", wikiContent, commitMsg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, models.IsErrWikiAlreadyExist(err))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check wiki reserved name", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
// test for reserved wiki name
|
||||||
|
err := AddWikiPage(doer, repo, "_edit", wikiContent, commitMsg)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.True(t, models.IsErrWikiReservedName(err))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepository_EditWikiPage(t *testing.T) {
|
||||||
|
const newWikiContent = "This is the new content"
|
||||||
|
const commitMsg = "Commit message"
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
for _, newWikiName := range []string{
|
||||||
|
"Home", // same name as before
|
||||||
|
"New home",
|
||||||
|
"New/name/with/slashes",
|
||||||
|
} {
|
||||||
|
models.PrepareTestEnv(t)
|
||||||
|
assert.NoError(t, EditWikiPage(doer, repo, "Home", newWikiName, newWikiContent, commitMsg))
|
||||||
|
|
||||||
|
// Now need to show that the page has been added:
|
||||||
|
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
masterTree, err := gitRepo.GetTree("master")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
wikiPath := NameToFilename(newWikiName)
|
||||||
|
entry, err := masterTree.GetTreeEntryByPath(wikiPath)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, wikiPath, entry.Name(), "%s not editted correctly", newWikiName)
|
||||||
|
|
||||||
|
if newWikiName != "Home" {
|
||||||
|
_, err := masterTree.GetTreeEntryByPath("Home.md")
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
||||||
|
gitRepo.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRepository_DeleteWikiPage(t *testing.T) {
|
||||||
|
models.PrepareTestEnv(t)
|
||||||
|
repo := models.AssertExistsAndLoadBean(t, &models.Repository{ID: 1}).(*models.Repository)
|
||||||
|
doer := models.AssertExistsAndLoadBean(t, &models.User{ID: 2}).(*models.User)
|
||||||
|
assert.NoError(t, DeleteWikiPage(doer, repo, "Home"))
|
||||||
|
|
||||||
|
// Now need to show that the page has been added:
|
||||||
|
gitRepo, err := git.OpenRepository(repo.WikiPath())
|
||||||
|
assert.NoError(t, err)
|
||||||
|
defer gitRepo.Close()
|
||||||
|
masterTree, err := gitRepo.GetTree("master")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
wikiPath := NameToFilename("Home")
|
||||||
|
_, err = masterTree.GetTreeEntryByPath(wikiPath)
|
||||||
|
assert.Error(t, err)
|
||||||
|
}
|
Reference in a new issue