diff --git a/modules/git/repo_commit.go b/modules/git/repo_commit.go index 2b780feb5c..153a116b06 100644 --- a/modules/git/repo_commit.go +++ b/modules/git/repo_commit.go @@ -84,6 +84,9 @@ func (repo *Repository) GetCommitByPath(relpath string) (*Commit, error) { if err != nil { return nil, err } + if len(commits) == 0 { + return nil, ErrNotExist{ID: relpath} + } return commits[0], nil } diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index f168334d9d..d1ea8f8ed6 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1791,6 +1791,7 @@ wiki.reserved_page = The wiki page name "%s" is reserved. wiki.pages = Pages wiki.last_updated = Last updated %s wiki.page_name_desc = Enter a name for this Wiki page. Some special names are: 'Home', '_Sidebar' and '_Footer'. +wiki.original_git_entry_tooltip = View original Git file instead of using friendly link. activity = Activity activity.period.filter_label = Period: diff --git a/routers/api/v1/repo/wiki.go b/routers/api/v1/repo/wiki.go index 764530a671..0b9a36ec47 100644 --- a/routers/api/v1/repo/wiki.go +++ b/routers/api/v1/repo/wiki.go @@ -58,10 +58,10 @@ func NewWikiPage(ctx *context.APIContext) { return } - wikiName := wiki_service.NormalizeWikiName(form.Title) + wikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { - form.Message = fmt.Sprintf("Add '%s'", form.Title) + form.Message = fmt.Sprintf("Add %q", form.Title) } content, err := base64.StdEncoding.DecodeString(form.ContentBase64) @@ -85,7 +85,7 @@ func NewWikiPage(ctx *context.APIContext) { wikiPage := getWikiPage(ctx, wikiName) if !ctx.Written() { - notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message) + notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message) ctx.JSON(http.StatusCreated, wikiPage) } } @@ -127,15 +127,15 @@ func EditWikiPage(ctx *context.APIContext) { form := web.GetForm(ctx).(*api.CreateWikiPageOptions) - oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) - newWikiName := wiki_service.NormalizeWikiName(form.Title) + oldWikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) + newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(newWikiName) == 0 { newWikiName = oldWikiName } if len(form.Message) == 0 { - form.Message = fmt.Sprintf("Update '%s'", newWikiName) + form.Message = fmt.Sprintf("Update %q", newWikiName) } content, err := base64.StdEncoding.DecodeString(form.ContentBase64) @@ -153,14 +153,12 @@ func EditWikiPage(ctx *context.APIContext) { wikiPage := getWikiPage(ctx, newWikiName) if !ctx.Written() { - notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message) + notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message) ctx.JSON(http.StatusOK, wikiPage) } } -func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { - title = wiki_service.NormalizeWikiName(title) - +func getWikiPage(ctx *context.APIContext, wikiName wiki_service.WebPath) *api.WikiPage { wikiRepo, commit := findWikiRepoCommit(ctx) if wikiRepo != nil { defer wikiRepo.Close() @@ -170,7 +168,7 @@ func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { } // lookup filename in wiki - get filecontent, real filename - content, pageFilename := wikiContentsByName(ctx, commit, title, false) + content, pageFilename := wikiContentsByName(ctx, commit, wikiName, false) if ctx.Written() { return nil } @@ -196,7 +194,7 @@ func getWikiPage(ctx *context.APIContext, title string) *api.WikiPage { } return &api.WikiPage{ - WikiPageMetaData: convert.ToWikiPageMetaData(title, lastCommit, ctx.Repo.Repository), + WikiPageMetaData: convert.ToWikiPageMetaData(wikiName, lastCommit, ctx.Repo.Repository), ContentBase64: content, CommitCount: commitsCount, Sidebar: sidebarContent, @@ -233,7 +231,7 @@ func DeleteWikiPage(ctx *context.APIContext) { // "404": // "$ref": "#/responses/notFound" - wikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + wikiName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil { if err.Error() == "file does not exist" { @@ -244,7 +242,7 @@ func DeleteWikiPage(ctx *context.APIContext) { return } - notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName) + notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName)) ctx.Status(http.StatusNoContent) } @@ -316,7 +314,7 @@ func ListWikiPages(ctx *context.APIContext) { ctx.Error(http.StatusInternalServerError, "GetCommit", err) return } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -361,7 +359,7 @@ func GetWikiPage(ctx *context.APIContext) { // "$ref": "#/responses/notFound" // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) wikiPage := getWikiPage(ctx, pageName) if !ctx.Written() { @@ -411,7 +409,7 @@ func ListPageRevisions(ctx *context.APIContext) { } // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName")) + pageName := wiki_service.WebPathFromRequest(ctx.Params(":pageName")) if len(pageName) == 0 { pageName = "Home" } @@ -502,9 +500,9 @@ func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string { // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName string, isSidebarOrFooter bool) (string, string) { - pageFilename := wiki_service.NameToFilename(wikiName) - entry, err := findEntryForFile(commit, pageFilename) +func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName wiki_service.WebPath, isSidebarOrFooter bool) (string, string) { + gitFilename := wiki_service.WebPathToGitPath(wikiName) + entry, err := findEntryForFile(commit, gitFilename) if err != nil { if git.IsErrNotExist(err) { if !isSidebarOrFooter { @@ -515,5 +513,5 @@ func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName st } return "", "" } - return wikiContentsByEntry(ctx, entry), pageFilename + return wikiContentsByEntry(ctx, entry), gitFilename } diff --git a/routers/web/repo/wiki.go b/routers/web/repo/wiki.go index 0c5c5eed7d..374d1bf2e0 100644 --- a/routers/web/repo/wiki.go +++ b/routers/web/repo/wiki.go @@ -68,9 +68,10 @@ func MustEnableWiki(ctx *context.Context) { // PageMeta wiki page meta information type PageMeta struct { - Name string - SubURL string - UpdatedUnix timeutil.TimeStamp + Name string + SubURL string + GitEntryName string + UpdatedUnix timeutil.TimeStamp } // findEntryForFile finds the tree entry for a target filepath. @@ -83,7 +84,7 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) return entry, nil } - // Then the unescaped, shortest alternative + // Then the unescaped, the shortest alternative var unescapedTarget string if unescapedTarget, err = url.QueryUnescape(target); err != nil { return nil, err @@ -124,16 +125,16 @@ func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte { // wikiContentsByName returns the contents of a wiki page, along with a boolean // indicating whether the page exists. Writes to ctx if an error occurs. -func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName string) ([]byte, *git.TreeEntry, string, bool) { - pageFilename := wiki_service.NameToFilename(wikiName) - entry, err := findEntryForFile(commit, pageFilename) +func wikiContentsByName(ctx *context.Context, commit *git.Commit, wikiName wiki_service.WebPath) ([]byte, *git.TreeEntry, string, bool) { + gitFilename := wiki_service.WebPathToGitPath(wikiName) + entry, err := findEntryForFile(commit, gitFilename) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findEntryForFile", err) return nil, nil, "", false } else if entry == nil { return nil, nil, "", true } - return wikiContentsByEntry(ctx, entry), entry, pageFilename, false + return wikiContentsByEntry(ctx, entry), entry, gitFilename, false } func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { @@ -162,7 +163,7 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { if !entry.IsRegular() { continue } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -175,22 +176,26 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) { } else if wikiName == "_Sidebar" || wikiName == "_Footer" { continue } + _, displayName := wiki_service.WebPathToUserTitle(wikiName) pages = append(pages, PageMeta{ - Name: wikiName, - SubURL: wiki_service.NameToSubURL(wikiName), + Name: displayName, + SubURL: wiki_service.WebPathToURLPath(wikiName), + GitEntryName: entry.Name(), }) } ctx.Data["Pages"] = pages - // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + // get requested page name + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName isSideBar := pageName == "_Sidebar" isFooter := pageName == "_Footer" @@ -328,14 +333,17 @@ func renderRevisionPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) } // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName + ctx.Data["Username"] = ctx.Repo.Owner.Name ctx.Data["Reponame"] = ctx.Repo.Repository.Name @@ -403,14 +411,16 @@ func renderEditPage(ctx *context.Context) { }() // get requested pagename - pageName := wiki_service.NormalizeWikiName(ctx.Params("*")) + pageName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(pageName) == 0 { pageName = "Home" } - ctx.Data["PageURL"] = wiki_service.NameToSubURL(pageName) - ctx.Data["old_title"] = pageName - ctx.Data["Title"] = pageName - ctx.Data["title"] = pageName + + _, displayName := wiki_service.WebPathToUserTitle(pageName) + ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName) + ctx.Data["old_title"] = displayName + ctx.Data["Title"] = displayName + ctx.Data["title"] = displayName // lookup filename in wiki - get filecontent, gitTree entry , real filename data, entry, _, noEntry := wikiContentsByName(ctx, commit, pageName) @@ -594,7 +604,7 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("GetCommit", err) return } - wikiName, err := wiki_service.FilenameToName(entry.Name()) + wikiName, err := wiki_service.GitPathToWebPath(entry.Name()) if err != nil { if repo_model.IsErrWikiInvalidFileName(err) { continue @@ -602,10 +612,12 @@ func WikiPages(ctx *context.Context) { ctx.ServerError("WikiFilenameToName", err) return } + _, displayName := wiki_service.WebPathToUserTitle(wikiName) pages = append(pages, PageMeta{ - Name: wikiName, - SubURL: wiki_service.NameToSubURL(wikiName), - UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()), + Name: displayName, + SubURL: wiki_service.WebPathToURLPath(wikiName), + GitEntryName: entry.Name(), + UpdatedUnix: timeutil.TimeStamp(c.Author.When.Unix()), }) } ctx.Data["Pages"] = pages @@ -631,12 +643,12 @@ func WikiRaw(ctx *context.Context) { return } - providedPath := ctx.Params("*") - + providedWebPath := wiki_service.WebPathFromRequest(ctx.Params("*")) + providedGitPath := wiki_service.WebPathToGitPath(providedWebPath) var entry *git.TreeEntry if commit != nil { // Try to find a file with that name - entry, err = findEntryForFile(commit, providedPath) + entry, err = findEntryForFile(commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -644,10 +656,8 @@ func WikiRaw(ctx *context.Context) { if entry == nil { // Try to find a wiki page with that name - providedPath = strings.TrimSuffix(providedPath, ".md") - - wikiPath := wiki_service.NameToFilename(providedPath) - entry, err = findEntryForFile(commit, wikiPath) + providedGitPath = strings.TrimSuffix(providedGitPath, ".md") + entry, err = findEntryForFile(commit, providedGitPath) if err != nil && !git.IsErrNotExist(err) { ctx.ServerError("findFile", err) return @@ -694,7 +704,7 @@ func NewWikiPost(ctx *context.Context) { return } - wikiName := wiki_service.NormalizeWikiName(form.Title) + wikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { form.Message = ctx.Tr("repo.editor.add", form.Title) @@ -713,9 +723,9 @@ func NewWikiPost(ctx *context.Context) { return } - notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Message) + notification.NotifyNewWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName), form.Message) - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(wikiName)) + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(wikiName)) } // EditWiki render wiki modify page @@ -745,8 +755,8 @@ func EditWikiPost(ctx *context.Context) { return } - oldWikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) - newWikiName := wiki_service.NormalizeWikiName(form.Title) + oldWikiName := wiki_service.WebPathFromRequest(ctx.Params("*")) + newWikiName := wiki_service.UserTitleToWebPath("", form.Title) if len(form.Message) == 0 { form.Message = ctx.Tr("repo.editor.update", form.Title) @@ -757,14 +767,14 @@ func EditWikiPost(ctx *context.Context) { return } - notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message) + notification.NotifyEditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(newWikiName), form.Message) - ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.NameToSubURL(newWikiName)) + ctx.Redirect(ctx.Repo.RepoLink + "/wiki/" + wiki_service.WebPathToURLPath(newWikiName)) } // DeleteWikiPagePost delete wiki page func DeleteWikiPagePost(ctx *context.Context) { - wikiName := wiki_service.NormalizeWikiName(ctx.Params("*")) + wikiName := wiki_service.WebPathFromRequest(ctx.Params("*")) if len(wikiName) == 0 { wikiName = "Home" } @@ -774,7 +784,7 @@ func DeleteWikiPagePost(ctx *context.Context) { return } - notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName) + notification.NotifyDeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, string(wikiName)) ctx.JSON(http.StatusOK, map[string]interface{}{ "redirect": ctx.Repo.RepoLink + "/wiki/", diff --git a/routers/web/repo/wiki_test.go b/routers/web/repo/wiki_test.go index 4699f5379a..e51820a520 100644 --- a/routers/web/repo/wiki_test.go +++ b/routers/web/repo/wiki_test.go @@ -6,6 +6,7 @@ package repo import ( "io" "net/http" + "net/url" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -24,7 +25,7 @@ const ( message = "Wiki commit message for unit tests" ) -func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName string) *git.TreeEntry { +func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) *git.TreeEntry { wikiRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) defer wikiRepo.Close() @@ -33,14 +34,14 @@ func wikiEntry(t *testing.T, repo *repo_model.Repository, wikiName string) *git. entries, err := commit.ListEntries() assert.NoError(t, err) for _, entry := range entries { - if entry.Name() == wiki_service.NameToFilename(wikiName) { + if entry.Name() == wiki_service.WebPathToGitPath(wikiName) { return entry } } return nil } -func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName string) string { +func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) string { entry := wikiEntry(t, repo, wikiName) if !assert.NotNil(t, entry) { return "" @@ -53,11 +54,11 @@ func wikiContent(t *testing.T, repo *repo_model.Repository, wikiName string) str return string(bytes) } -func assertWikiExists(t *testing.T, repo *repo_model.Repository, wikiName string) { +func assertWikiExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { assert.NotNil(t, wikiEntry(t, repo, wikiName)) } -func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName string) { +func assertWikiNotExists(t *testing.T, repo *repo_model.Repository, wikiName wiki_service.WebPath) { assert.Nil(t, wikiEntry(t, repo, wikiName)) } @@ -124,8 +125,8 @@ func TestNewWikiPost(t *testing.T) { }) NewWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - assertWikiExists(t, ctx.Repo.Repository, title) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content) + assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) } } @@ -176,8 +177,8 @@ func TestEditWikiPost(t *testing.T) { }) EditWikiPost(ctx) assert.EqualValues(t, http.StatusSeeOther, ctx.Resp.Status()) - assertWikiExists(t, ctx.Repo.Repository, title) - assert.Equal(t, wikiContent(t, ctx.Repo.Repository, title), content) + assertWikiExists(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)) + assert.Equal(t, wikiContent(t, ctx.Repo.Repository, wiki_service.UserTitleToWebPath("", title)), content) if title != "Home" { assertWikiNotExists(t, ctx.Repo.Repository, "Home") } @@ -201,17 +202,21 @@ func TestWikiRaw(t *testing.T) { "images/jpeg.jpg": "image/jpeg", "Page With Spaced Name": "text/plain; charset=utf-8", "Page-With-Spaced-Name": "text/plain; charset=utf-8", - "Page With Spaced Name.md": "text/plain; charset=utf-8", + "Page With Spaced Name.md": "", // there is no "Page With Spaced Name.md" in repo "Page-With-Spaced-Name.md": "text/plain; charset=utf-8", } { unittest.PrepareTestEnv(t) - ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+filepath) + ctx := test.MockContext(t, "user2/repo1/wiki/raw/"+url.PathEscape(filepath)) ctx.SetParams("*", filepath) test.LoadUser(t, ctx, 2) test.LoadRepo(t, ctx, 1) WikiRaw(ctx) - assert.EqualValues(t, http.StatusOK, ctx.Resp.Status()) - assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type")) + if filetype == "" { + assert.EqualValues(t, http.StatusNotFound, ctx.Resp.Status(), "filepath: %s", filepath) + } else { + assert.EqualValues(t, http.StatusOK, ctx.Resp.Status(), "filepath: %s", filepath) + assert.EqualValues(t, filetype, ctx.Resp.Header().Get("Content-Type"), "filepath: %s", filepath) + } } } diff --git a/services/convert/wiki.go b/services/convert/wiki.go index 20d76162c7..1f04843483 100644 --- a/services/convert/wiki.go +++ b/services/convert/wiki.go @@ -48,12 +48,13 @@ func ToWikiCommitList(commits []*git.Commit, total int64) *api.WikiCommitList { } // ToWikiPageMetaData converts meta information to a WikiPageMetaData -func ToWikiPageMetaData(title string, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { - suburl := wiki_service.NameToSubURL(title) +func ToWikiPageMetaData(wikiName wiki_service.WebPath, lastCommit *git.Commit, repo *repo_model.Repository) *api.WikiPageMetaData { + subURL := string(wikiName) + _, title := wiki_service.WebPathToUserTitle(wikiName) return &api.WikiPageMetaData{ Title: title, - HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", suburl), - SubURL: suburl, + HTMLURL: util.URLJoin(repo.HTMLURL(), "wiki", subURL), + SubURL: subURL, LastCommit: ToWikiCommit(lastCommit), } } diff --git a/services/wiki/wiki.go b/services/wiki/wiki.go index e5cb2db02b..9ceb8e5817 100644 --- a/services/wiki/wiki.go +++ b/services/wiki/wiki.go @@ -7,7 +7,6 @@ package wiki import ( "context" "fmt" - "net/url" "os" "strings" @@ -19,61 +18,17 @@ import ( "code.gitea.io/gitea/modules/log" repo_module "code.gitea.io/gitea/modules/repository" "code.gitea.io/gitea/modules/sync" - "code.gitea.io/gitea/modules/util" asymkey_service "code.gitea.io/gitea/services/asymkey" ) -var ( - reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"} - // TODO: use clustered lock (unique queue? or *abuse* cache) - wikiWorkingPool = sync.NewExclusivePool() -) +// TODO: use clustered lock (unique queue? or *abuse* cache) +var wikiWorkingPool = sync.NewExclusivePool() const ( DefaultRemote = "origin" DefaultBranch = "master" ) -func nameAllowed(name string) error { - if util.SliceContainsString(reservedWikiNames, name) { - return repo_model.ErrWikiReservedName{ - Title: name, - } - } - return nil -} - -// NameToSubURL converts a wiki name to its corresponding sub-URL. -func NameToSubURL(name string) string { - return url.PathEscape(strings.ReplaceAll(name, " ", "-")) -} - -// NormalizeWikiName normalizes a wiki name -func NormalizeWikiName(name string) string { - return strings.ReplaceAll(name, "-", " ") -} - -// NameToFilename converts a wiki name to its corresponding filename. -func NameToFilename(name string) string { - name = strings.ReplaceAll(name, " ", "-") - 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 "", repo_model.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(ctx context.Context, repo *repo_model.Repository) error { @@ -91,20 +46,20 @@ func InitWiki(ctx context.Context, repo *repo_model.Repository) error { return nil } -// prepareWikiFileName try to find a suitable file path with file name by the given raw wiki name. +// prepareGitPath try to find a suitable file path with file name by the given raw wiki name. // return: existence, prepared file path with name, error -func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string, error) { - unescaped := wikiName + ".md" - escaped := NameToFilename(wikiName) +func prepareGitPath(gitRepo *git.Repository, wikiPath WebPath) (bool, string, error) { + unescaped := string(wikiPath) + ".md" + gitPath := WebPathToGitPath(wikiPath) // Look for both files - filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, escaped) + filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, gitPath) if err != nil { if strings.Contains(err.Error(), "Not a valid object name master") { - return false, escaped, nil + return false, gitPath, nil } log.Error("%v", err) - return false, escaped, err + return false, gitPath, err } foundEscaped := false @@ -113,18 +68,18 @@ func prepareWikiFileName(gitRepo *git.Repository, wikiName string) (bool, string case unescaped: // if we find the unescaped file return it return true, unescaped, nil - case escaped: + case gitPath: foundEscaped = true } } // If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility. - return foundEscaped, escaped, nil + return foundEscaped, gitPath, nil } // updateWikiPage adds a new page or edits an existing page in repository wiki. -func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) { - if err = nameAllowed(newWikiName); err != nil { +func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string, isNew bool) (err error) { + if err = validateWebPath(newWikiName); err != nil { return err } wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) @@ -157,24 +112,24 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err := git.Clone(ctx, 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 (%w)", repo.FullName(), err) + return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) } gitRepo, err := git.OpenRepository(ctx, 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 %w", basePath, err) + return fmt.Errorf("failed to open new temporary repository in: %s %w", 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 %w", basePath, err) + return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err) } } - isWikiExist, newWikiPath, err := prepareWikiFileName(gitRepo, newWikiName) + isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, newWikiName) if err != nil { return err } @@ -190,7 +145,7 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model isOldWikiExist := true oldWikiPath := newWikiPath if oldWikiName != newWikiName { - isOldWikiExist, oldWikiPath, err = prepareWikiFileName(gitRepo, oldWikiName) + isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, oldWikiName) if err != nil { return err } @@ -271,18 +226,18 @@ func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model } // AddWikiPage adds a new wiki page with a given wikiPath. -func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName, content, message string) error { +func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath, content, message string) error { return updateWikiPage(ctx, doer, repo, "", wikiName, content, message, true) } // EditWikiPage updates a wiki page identified by its wikiPath, // optionally also changing wikiPath. -func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName, content, message string) error { +func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string) error { return updateWikiPage(ctx, doer, repo, oldWikiName, newWikiName, content, message, false) } // DeleteWikiPage deletes a wiki page identified by its path. -func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName string) (err error) { +func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath) (err error) { wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID)) defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID)) @@ -306,22 +261,22 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model Branch: DefaultBranch, }); err != nil { log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err) - return fmt.Errorf("Failed to clone repository: %s (%w)", repo.FullName(), err) + return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err) } gitRepo, err := git.OpenRepository(ctx, 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 %w", basePath, err) + return fmt.Errorf("failed to open new temporary repository in: %s %w", 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 %w", basePath, err) + return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err) } - found, wikiPath, err := prepareWikiFileName(gitRepo, wikiName) + found, wikiPath, err := prepareGitPath(gitRepo, wikiName) if err != nil { return err } @@ -340,7 +295,7 @@ func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model if err != nil { return err } - message := "Delete page '" + wikiName + "'" + message := fmt.Sprintf("Delete page %q", wikiName) commitTreeOpts := git.CommitTreeOpts{ Message: message, Parents: []string{"HEAD"}, diff --git a/services/wiki/wiki_path.go b/services/wiki/wiki_path.go new file mode 100644 index 0000000000..45c6a5a84e --- /dev/null +++ b/services/wiki/wiki_path.go @@ -0,0 +1,153 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "net/url" + "path" + "strings" + + repo_model "code.gitea.io/gitea/models/repo" + "code.gitea.io/gitea/modules/util" +) + +// To define the wiki related concepts: +// * Display Segment: the text what user see for a wiki page (aka, the title): +// - "Home Page" +// - "100% Free" +// - "2000-01-02 meeting" +// * Web Path: +// - "/wiki/Home-Page" +// - "/wiki/100%25+Free" +// - "/wiki/2000-01-02+meeting.-" +// - If a segment has a suffix "DashMarker(.-)", it means that there is no dash-space conversion for this segment. +// - If a WebPath is a "*.md" pattern, then use it directly as GitPath, to make users can access the raw file. +// * Git Path (only space doesn't need to be escaped): +// - "/.wiki.git/Home-Page.md" +// - "/.wiki.git/100%25 Free.md" +// - "/.wiki.git/2000-01-02 meeting.-.md" +// TODO: support subdirectory in the future +// +// Although this package now has the ablity to support subdirectory, but the route package doesn't: +// * Double-escaping problem: the URL "/wiki/abc%2Fdef" becomes "/wiki/abc/def" by ctx.Params, which is incorrect +// * The old wiki code's behavior is always using %2F, instead of subdirectory, so there are a lot of legacy "%2F" files in user wikis. + +type WebPath string + +var reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"} + +func validateWebPath(name WebPath) error { + for _, s := range WebPathSegments(name) { + if util.SliceContainsString(reservedWikiNames, s) { + return repo_model.ErrWikiReservedName{Title: s} + } + } + return nil +} + +func hasDashMarker(s string) bool { + return strings.HasSuffix(s, ".-") +} + +func removeDashMarker(s string) string { + return strings.TrimSuffix(s, ".-") +} + +func addDashMarker(s string) string { + return s + ".-" +} + +func unescapeSegment(s string) (string, error) { + if hasDashMarker(s) { + s = removeDashMarker(s) + } else { + s = strings.ReplaceAll(s, "-", " ") + } + unescaped, err := url.QueryUnescape(s) + if err != nil { + return s, err // un-escaping failed, but it's still safe to return the original string, because it is only a title for end users + } + return unescaped, nil +} + +func escapeSegToWeb(s string, hadDashMarker bool) string { + if hadDashMarker || strings.Contains(s, "-") { + s = addDashMarker(s) + } else { + s = strings.ReplaceAll(s, " ", "-") + } + s = url.QueryEscape(s) + return s +} + +func WebPathSegments(s WebPath) []string { + a := strings.Split(string(s), "/") + for i := range a { + a[i], _ = unescapeSegment(a[i]) + } + return a +} + +func WebPathToGitPath(s WebPath) string { + if strings.HasSuffix(string(s), ".md") { + return string(s) + } + + a := strings.Split(string(s), "/") + for i := range a { + shouldAddDashMarker := hasDashMarker(a[i]) + a[i], _ = unescapeSegment(a[i]) + a[i] = escapeSegToWeb(a[i], shouldAddDashMarker) + a[i] = strings.ReplaceAll(a[i], "%20", " ") // space is safe to be kept in git path + a[i] = strings.ReplaceAll(a[i], "+", " ") + } + return strings.Join(a, "/") + ".md" +} + +func GitPathToWebPath(s string) (wp WebPath, err error) { + if !strings.HasSuffix(s, ".md") { + return "", repo_model.ErrWikiInvalidFileName{FileName: s} + } + s = strings.TrimSuffix(s, ".md") + a := strings.Split(s, "/") + for i := range a { + shouldAddDashMarker := hasDashMarker(a[i]) + if a[i], err = unescapeSegment(a[i]); err != nil { + return "", err + } + a[i] = escapeSegToWeb(a[i], shouldAddDashMarker) + } + return WebPath(strings.Join(a, "/")), nil +} + +func WebPathToUserTitle(s WebPath) (dir, display string) { + dir = path.Dir(string(s)) + display = path.Base(string(s)) + display = strings.TrimSuffix(display, ".md") + display, _ = unescapeSegment(display) + return dir, display +} + +func WebPathToURLPath(s WebPath) string { + return string(s) +} + +func WebPathFromRequest(s string) WebPath { + s = util.PathJoinRelX(s) + // The old wiki code's behavior is always using %2F, instead of subdirectory. + s = strings.ReplaceAll(s, "/", "%2F") + return WebPath(s) +} + +func UserTitleToWebPath(base, title string) WebPath { + // TODO: ctx.Params does un-escaping, so the URL "/wiki/abc%2Fdef" becomes "wiki path = `abc/def`", which is incorrect. + // And the old wiki code's behavior is always using %2F, instead of subdirectory. + // So we do not add the support for writing slashes in title at the moment. + title = strings.TrimSpace(title) + title = util.PathJoinRelX(base, escapeSegToWeb(title, false)) + if title == "" || title == "." { + title = "unnamed" + } + return WebPath(title) +} diff --git a/services/wiki/wiki_test.go b/services/wiki/wiki_test.go index 268d8848c5..716ea6104a 100644 --- a/services/wiki/wiki_test.go +++ b/services/wiki/wiki_test.go @@ -4,7 +4,9 @@ package wiki import ( + "math/rand" "path/filepath" + "strings" "testing" repo_model "code.gitea.io/gitea/models/repo" @@ -21,91 +23,113 @@ func TestMain(m *testing.M) { }) } -func TestWikiNameToSubURL(t *testing.T) { +func TestWebPathSegments(t *testing.T) { + a := WebPathSegments("a%2Fa/b+c/d-e/f-g.-") + assert.EqualValues(t, []string{"a/a", "b c", "d e", "f-g"}, a) +} + +func TestUserTitleToWebPath(t *testing.T) { type test struct { - Expected string - WikiName string + Expected string + UserTitle 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"}, + {"wiki-name.-", "wiki-name"}, + {"the+wiki-name.-", "the wiki-name"}, + {"a%2Fb", "a/b"}, + {"a%25b", "a%b"}, } { - assert.Equal(t, test.Expected, NameToSubURL(test.WikiName)) + assert.EqualValues(t, test.Expected, UserTitleToWebPath("", test.UserTitle)) } } -func TestNormalizeWikiName(t *testing.T) { +func TestWebPathToDisplayName(t *testing.T) { type test struct { Expected string - WikiName string + WebPath WebPath } 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"}, + {"wiki-name", "wiki-name.-"}, + {"name with / slash", "name-with %2F slash"}, + {"name with % percent", "name-with %25 percent"}, + {"2000-01-02 meeting", "2000-01-02+meeting.-.md"}, } { - assert.Equal(t, test.Expected, NormalizeWikiName(test.WikiName)) + _, displayName := WebPathToUserTitle(test.WebPath) + assert.EqualValues(t, test.Expected, displayName) } } -func TestWikiNameToFilename(t *testing.T) { +func TestWebPathToGitPath(t *testing.T) { type test struct { Expected string - WikiName string + WikiName WebPath } 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"}, + {"wiki-name.md", "wiki%20name"}, + {"wiki-name.md", "wiki+name"}, + {"wiki%20name.md", "wiki%20name.md"}, + {"2000-01-02-meeting.md", "2000-01-02+meeting"}, + {"2000-01-02 meeting.-.md", "2000-01-02%20meeting.-"}, } { - assert.Equal(t, test.Expected, NameToFilename(test.WikiName)) + assert.EqualValues(t, test.Expected, WebPathToGitPath(test.WikiName)) } } -func TestWikiFilenameToName(t *testing.T) { +func TestGitPathToWebPath(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"}, + {"hello-world", "hello-world.md"}, // this shouldn't happen, because it should always have a ".-" suffix + {"hello-world", "hello world.md"}, + {"hello-world.-", "hello-world.-.md"}, + {"hello+world.-", "hello world.-.md"}, + {"symbols-%2F", "symbols %2F.md"}, } { - name, err := FilenameToName(test.Filename) + name, err := GitPathToWebPath(test.Filename) assert.NoError(t, err) - assert.Equal(t, test.Expected, name) + assert.EqualValues(t, test.Expected, name) } for _, badFilename := range []string{ "nofileextension", "wrongfileextension.txt", } { - _, err := FilenameToName(badFilename) + _, err := GitPathToWebPath(badFilename) assert.Error(t, err) assert.True(t, repo_model.IsErrWikiInvalidFileName(err)) } - _, err := FilenameToName("badescaping%%.md") + _, err := GitPathToWebPath("badescaping%%.md") assert.Error(t, err) assert.False(t, repo_model.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 TestUserWebGitPathConsistency(t *testing.T) { + maxLen := 20 + b := make([]byte, maxLen) + for i := 0; i < 1000; i++ { + l := rand.Intn(maxLen) + for j := 0; j < l; j++ { + r := rand.Intn(0x80-0x20) + 0x20 + b[j] = byte(r) + } + + userTitle := strings.TrimSpace(string(b[:l])) + if userTitle == "" || userTitle == "." { + continue + } + webPath := UserTitleToWebPath("", userTitle) + gitPath := WebPathToGitPath(webPath) + + webPath1, _ := GitPathToWebPath(gitPath) + _, userTitle1 := WebPathToUserTitle(webPath1) + gitPath1 := WebPathToGitPath(webPath1) + + assert.EqualValues(t, userTitle, userTitle1, "UserTitle for userTitle: %q", userTitle) + assert.EqualValues(t, webPath, webPath1, "WebPath for userTitle: %q", userTitle) + assert.EqualValues(t, gitPath, gitPath1, "GitPath for userTitle: %q", userTitle) } } @@ -127,24 +151,23 @@ func TestRepository_AddWikiPage(t *testing.T) { const commitMsg = "Commit message" repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1}) doer := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - for _, wikiName := range []string{ + for _, userTitle := range []string{ "Another page", "Here's a and a/slash", } { - wikiName := wikiName - t.Run("test wiki exist: "+wikiName, func(t *testing.T) { - t.Parallel() - assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, wikiName, wikiContent, commitMsg)) + t.Run("test wiki exist: "+userTitle, func(t *testing.T) { + webPath := UserTitleToWebPath("", userTitle) + assert.NoError(t, AddWikiPage(git.DefaultContext, doer, repo, webPath, wikiContent, commitMsg)) // Now need to show that the page has been added: gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) defer gitRepo.Close() masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) - wikiPath := NameToFilename(wikiName) - entry, err := masterTree.GetTreeEntryByPath(wikiPath) + gitPath := WebPathToGitPath(webPath) + entry, err := masterTree.GetTreeEntryByPath(gitPath) assert.NoError(t, err) - assert.Equal(t, wikiPath, entry.Name(), "%s not added correctly", wikiName) + assert.EqualValues(t, gitPath, entry.Name(), "%s not added correctly", userTitle) }) } @@ -177,18 +200,19 @@ func TestRepository_EditWikiPage(t *testing.T) { "New home", "New/name/with/slashes", } { + webPath := UserTitleToWebPath("", newWikiName) unittest.PrepareTestEnv(t) - assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", newWikiName, newWikiContent, commitMsg)) + assert.NoError(t, EditWikiPage(git.DefaultContext, doer, repo, "Home", webPath, newWikiContent, commitMsg)) // Now need to show that the page has been added: gitRepo, err := git.OpenRepository(git.DefaultContext, repo.WikiPath()) assert.NoError(t, err) masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) - wikiPath := NameToFilename(newWikiName) - entry, err := masterTree.GetTreeEntryByPath(wikiPath) + gitPath := WebPathToGitPath(webPath) + entry, err := masterTree.GetTreeEntryByPath(gitPath) assert.NoError(t, err) - assert.Equal(t, wikiPath, entry.Name(), "%s not edited correctly", newWikiName) + assert.EqualValues(t, gitPath, entry.Name(), "%s not edited correctly", newWikiName) if newWikiName != "Home" { _, err := masterTree.GetTreeEntryByPath("Home.md") @@ -210,8 +234,8 @@ func TestRepository_DeleteWikiPage(t *testing.T) { defer gitRepo.Close() masterTree, err := gitRepo.GetTree(DefaultBranch) assert.NoError(t, err) - wikiPath := NameToFilename("Home") - _, err = masterTree.GetTreeEntryByPath(wikiPath) + gitPath := WebPathToGitPath("Home") + _, err = masterTree.GetTreeEntryByPath(gitPath) assert.Error(t, err) } @@ -240,16 +264,11 @@ func TestPrepareWikiFileName(t *testing.T) { existence: false, wikiPath: "home-of-and-%26-or-wiki-page%21.md", wantErr: false, - }, { - name: "found unescaped cases", - arg: "Unescaped File", - existence: true, - wikiPath: "Unescaped File.md", - wantErr: false, }} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - existence, newWikiPath, err := prepareWikiFileName(gitRepo, tt.arg) + webPath := UserTitleToWebPath("", tt.arg) + existence, newWikiPath, err := prepareGitPath(gitRepo, webPath) if (err != nil) != tt.wantErr { assert.NoError(t, err) return @@ -261,7 +280,7 @@ func TestPrepareWikiFileName(t *testing.T) { t.Errorf("expect to find an escaped file but we could not detect one") } } - assert.Equal(t, tt.wikiPath, newWikiPath) + assert.EqualValues(t, tt.wikiPath, newWikiPath) }) } } @@ -279,8 +298,8 @@ func TestPrepareWikiFileName_FirstPage(t *testing.T) { defer gitRepo.Close() assert.NoError(t, err) - existence, newWikiPath, err := prepareWikiFileName(gitRepo, "Home") + existence, newWikiPath, err := prepareGitPath(gitRepo, "Home") assert.False(t, existence) assert.NoError(t, err) - assert.Equal(t, "Home.md", newWikiPath) + assert.EqualValues(t, "Home.md", newWikiPath) } diff --git a/templates/repo/wiki/pages.tmpl b/templates/repo/wiki/pages.tmpl index 743574d0a5..97abf9b543 100644 --- a/templates/repo/wiki/pages.tmpl +++ b/templates/repo/wiki/pages.tmpl @@ -3,22 +3,21 @@ {{template "repo/header" .}}

-
- {{.locale.Tr "repo.wiki.pages"}} -
-
+ {{.locale.Tr "repo.wiki.pages"}} + {{if and .CanWriteWiki (not .IsRepositoryMirror)}} {{.locale.Tr "repo.wiki.new_page_button"}} {{end}} -
+

- +
{{range .Pages}} {{$timeSince := TimeSinceUnix .UpdatedUnix $.locale}} diff --git a/templates/repo/wiki/view.tmpl b/templates/repo/wiki/view.tmpl index 318006d96e..123511d011 100644 --- a/templates/repo/wiki/view.tmpl +++ b/templates/repo/wiki/view.tmpl @@ -5,34 +5,32 @@
-
-
{{svg "octicon-file"}} {{.Name}} + {{svg "octicon-chevron-right"}} {{$.locale.Tr "repo.wiki.last_updated" $timeSince | Safe}}