Compare commits

...

1 Commits

Author SHA1 Message Date
otto f2d7046f8f feat(Wiki模块支持文件夹): 新增Wiki文件夹创建接口;Wiki创建接口调整为支持文件夹;
1、新增文件夹时默认在文件夹下创建.keep文件,防止空目录无法提交的情况
2、新增文件夹时可传入文件夹相对路径用来支持多级目录的创建
3、新增Wiki时可传入文件夹相对路径,使Wiki支持文件夹
2023-02-23 15:14:16 +08:00
4 changed files with 1023 additions and 327 deletions

View File

@ -0,0 +1,22 @@
package structs
// CreateWikiDirOptions form for creating wiki dir
type CreateWikiDirOptions struct {
// dir name. leave empty to keep unchanged
DirName string `json:"dir_name"`
// optional commit message summarizing the change
Message string `json:"message"`
Path string `json:"path"`
}
// CreateWikiPageOptions form for creating wiki
type CreateWikiPageOptions struct {
// page title. leave empty to keep unchanged
Title string `json:"title"`
// content must be base64 encoded
ContentBase64 string `json:"content_base64"`
// optional commit message summarizing the change
Message string `json:"message"`
Path string `json:"path"`
}

View File

@ -79,15 +79,26 @@ func Routers(ctx gocontext.Context) *web.Route {
m.Group("/tags", func() {
m.Get("", repo.ListTags)
}, reqRepoReader(unit_model.TypeCode), context.ReferencesGitRepo(true))
m.Group("/wikies", func() {
m.Combo("").Get(repo.ListWikiPages).
Post(bind(hat_api.WikiOption{}), repo.CreateWiki)
m.Group("/{page}", func() {
m.Combo("").Get(repo.GetWiki).
Patch(bind(hat_api.WikiOption{}), repo.EditWiki).
Delete(repo.DeleteWiki)
})
})
//m.Group("/wikies", func() {
// m.Combo("").
// Get(repo.ListWikiPages).
// Post(bind(hat_api.WikiOption{}), repo.CreateWiki)
// m.Group("/{page}", func() {
// m.Combo("").Get(repo.GetWiki).
// Patch(bind(hat_api.WikiOption{}), repo.EditWiki).
// Delete(repo.DeleteWiki)
// })
//})
m.Group("/wiki", func() {
//m.Combo("/page/{pageName}").
// Get(repo.GetWikiPage).
// Patch(mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(api.CreateWikiPageOptions{}), repo.EditWikiPage).
// Delete(mustNotBeArchived, reqRepoWriter(unit.TypeWiki), repo.DeleteWikiPage)
//m.Get("/revisions/{pageName}", repo.ListPageRevisions)
m.Post("/new", mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(hat_api.CreateWikiPageOptions{}), repo.NewWikiPage)
m.Post("/new/dir", mustNotBeArchived, reqRepoWriter(unit.TypeWiki), bind(hat_api.CreateWikiDirOptions{}), repo.NewWikiDir)
//m.Get("/pages", repo.ListWikiPages)
}, mustEnableWiki)
m.Group("/readme", func() {
m.Get("", repo.GetReadmeContents)
m.Get("/*", repo.GetReadmeContentsByPath)
@ -179,6 +190,20 @@ func Routers(ctx gocontext.Context) *web.Route {
return m
}
func mustNotBeArchived(ctx *context.APIContext) {
if ctx.Repo.Repository.IsArchived {
ctx.NotFound()
return
}
}
func mustEnableWiki(ctx *context.APIContext) {
if !(ctx.Repo.CanRead(unit.TypeWiki)) {
ctx.NotFound()
return
}
}
func securityHeaders() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {

View File

@ -1,57 +1,406 @@
// Copyright 2021 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 repo
import (
"bytes"
"io"
"encoding/base64"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
"github.com/russross/blackfriday/v2"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/convert"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
"code.gitea.io/gitea/modules/markup/markdown"
"code.gitea.io/gitea/modules/notification"
"code.gitea.io/gitea/modules/setting"
api "code.gitea.io/gitea/modules/structs"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web"
wiki_service "code.gitea.io/gitea/services/wiki"
hat_git "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/git"
hat_api "code.gitlink.org.cn/Gitlink/gitea_hat.git/modules/structs"
wiki_service "code.gitlink.org.cn/Gitlink/gitea_hat.git/services/wiki"
)
func ListWikiPages(ctx *context.APIContext) {
if !ctx.Repo.Repository.HasWiki() {
// NewWikiPage response for wiki create request
func NewWikiPage(ctx *context.APIContext) {
// swagger:operation POST /repos/{owner}/{repo}/wiki/new repository repoCreateWikiPage
// ---
// summary: Create a wiki page
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateWikiPageOptions"
// responses:
// "201":
// "$ref": "#/responses/WikiPage"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*hat_api.CreateWikiPageOptions)
if util.IsEmptyString(form.Title) {
ctx.Error(http.StatusBadRequest, "emptyTitle", nil)
return
}
wikiCloneWiki := ctx.Repo.Repository.WikiCloneLink()
wikiRepo, commit, err := findWikiRepoCommit(ctx.Context)
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
wikiName := wiki_service.NormalizeWikiName(form.Title)
if len(form.Message) == 0 {
form.Message = fmt.Sprintf("Add '%s'", form.Title)
}
if len(form.Path) > 0 {
path := form.Path
if path[len(path)-1] != '/' {
form.Path = form.Path + "/"
}
}()
}
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.ServerError("findWikiRepoCommit", err)
ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err)
return
}
form.ContentBase64 = string(content)
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Path, form.ContentBase64, form.Message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiReservedName", err)
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiAlreadyExists", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddWikiPage", err)
}
return
}
wikiPage := getWikiPage(ctx, wikiName, form.Path)
if !ctx.Written() {
notification.NotifyNewWikiPage(ctx.Doer, ctx.Repo.Repository, wikiName, form.Message)
ctx.JSON(http.StatusCreated, wikiPage)
}
}
func NewWikiDir(ctx *context.APIContext) {
form := web.GetForm(ctx).(*hat_api.CreateWikiDirOptions)
if util.IsEmptyString(form.DirName) {
ctx.Error(http.StatusBadRequest, "emptyDirName", nil)
return
}
dirName := wiki_service.NormalizeWikiName(form.DirName)
if len(form.Message) == 0 {
form.Message = fmt.Sprintf("Add Dir '%s'", form.DirName)
}
if len(form.Path) > 0 {
path := form.Path
if path[len(path)-1] != '/' {
form.Path = form.Path + "/"
}
}
if err := wiki_service.AddWikiDir(ctx, ctx.Doer, ctx.Repo.Repository, dirName, form.Path, form.Message); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiDirReservedName", err)
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Error(http.StatusBadRequest, "IsErrWikiDirAlreadyExists", err)
} else {
ctx.Error(http.StatusInternalServerError, "AddWikiDir", err)
}
return
}
wikiPage := getWikiDir(ctx, dirName, form.Path)
if !ctx.Written() {
notification.NotifyNewWikiPage(ctx.Doer, ctx.Repo.Repository, dirName, form.Message)
ctx.JSON(http.StatusCreated, wikiPage)
}
}
// EditWikiPage response for wiki modify request
func EditWikiPage(ctx *context.APIContext) {
// swagger:operation PATCH /repos/{owner}/{repo}/wiki/page/{pageName} repository repoEditWikiPage
// ---
// summary: Edit a wiki page
// consumes:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// - name: body
// in: body
// schema:
// "$ref": "#/definitions/CreateWikiPageOptions"
// responses:
// "200":
// "$ref": "#/responses/WikiPage"
// "400":
// "$ref": "#/responses/error"
// "403":
// "$ref": "#/responses/forbidden"
form := web.GetForm(ctx).(*hat_api.CreateWikiPageOptions)
oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName"))
newWikiName := wiki_service.NormalizeWikiName(form.Title)
if len(newWikiName) == 0 {
newWikiName = oldWikiName
}
if len(form.Message) == 0 {
form.Message = fmt.Sprintf("Update '%s'", newWikiName)
}
if len(form.Path) > 0 {
path := form.Path
if path[len(path)-1] != '/' {
form.Path = form.Path + "/"
}
}
content, err := base64.StdEncoding.DecodeString(form.ContentBase64)
if err != nil {
ctx.Error(http.StatusBadRequest, "invalid base64 encoding of content", err)
return
}
form.ContentBase64 = string(content)
if err := wiki_service.EditWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.ContentBase64, form.Message); err != nil {
ctx.Error(http.StatusInternalServerError, "EditWikiPage", err)
return
}
wikiPage := getWikiPage(ctx, newWikiName, form.Path)
if !ctx.Written() {
notification.NotifyEditWikiPage(ctx.Doer, ctx.Repo.Repository, newWikiName, form.Message)
ctx.JSON(http.StatusOK, wikiPage)
}
}
func getWikiPage(ctx *context.APIContext, title, path string) *api.WikiPage {
title = wiki_service.NormalizeWikiName(title)
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return nil
}
// lookup filename in wiki - get filecontent, real filename
content, pageFilename := wikiContentsByName(ctx, commit, title, path, false)
if ctx.Written() {
return nil
}
sidebarContent, _ := wikiContentsByName(ctx, commit, "_Sidebar", path, true)
if ctx.Written() {
return nil
}
footerContent, _ := wikiContentsByName(ctx, commit, "_Footer", path, true)
if ctx.Written() {
return nil
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err)
return nil
}
return &api.WikiPage{
WikiPageMetaData: convert.ToWikiPageMetaData(title, lastCommit, ctx.Repo.Repository),
ContentBase64: content,
CommitCount: commitsCount,
Sidebar: sidebarContent,
Footer: footerContent,
}
}
func getWikiDir(ctx *context.APIContext, dirName, path string) *api.WikiPage {
dirName = wiki_service.NormalizeWikiName(dirName)
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return nil
}
// lookup filename in wiki - get filecontent, real filename
content, pageFilename := wikiDirByName(ctx, commit, dirName, path, false)
if ctx.Written() {
return nil
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
// Get last change information.
lastCommit, err := wikiRepo.GetCommitByPath(pageFilename)
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetCommitByPath", err)
return nil
}
return &api.WikiPage{
WikiPageMetaData: convert.ToWikiPageMetaData(dirName, lastCommit, ctx.Repo.Repository),
ContentBase64: content,
CommitCount: commitsCount,
}
}
// DeleteWikiPage delete wiki page
func DeleteWikiPage(ctx *context.APIContext) {
// swagger:operation DELETE /repos/{owner}/{repo}/wiki/page/{pageName} repository repoDeleteWikiPage
// ---
// summary: Delete a wiki page
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// responses:
// "204":
// "$ref": "#/responses/empty"
// "403":
// "$ref": "#/responses/forbidden"
// "404":
// "$ref": "#/responses/notFound"
wikiName := wiki_service.NormalizeWikiName(ctx.Params(":pageName"))
if err := wiki_service.DeleteWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName); err != nil {
if err.Error() == "file does not exist" {
ctx.NotFound(err)
return
}
ctx.Error(http.StatusInternalServerError, "DeleteWikiPage", err)
return
}
notification.NotifyDeleteWikiPage(ctx.Doer, ctx.Repo.Repository, wikiName)
ctx.Status(http.StatusNoContent)
}
// ListWikiPages get wiki pages list
func ListWikiPages(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/pages repository repoGetWikiPages
// ---
// summary: Get all wiki pages
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// - name: limit
// in: query
// description: page size of results
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WikiPageList"
// "404":
// "$ref": "#/responses/notFound"
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return
}
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
limit := ctx.FormInt("limit")
if limit <= 1 {
limit = setting.API.DefaultPagingNum
}
skip := (page - 1) * limit
max := page * limit
entries, err := commit.ListEntries()
if err != nil {
ctx.ServerError("ListEntries", err)
return
}
pages := make([]hat_api.WikiesResponse, 0, len(entries))
for _, entry := range entries {
if !entry.IsRegular() {
pages := make([]*api.WikiPageMetaData, 0, len(entries))
for i, entry := range entries {
if i < skip || i >= max || !entry.IsRegular() {
continue
}
lastCommit, firstCommit, err := hat_git.GetFirstAndLastCommitByPath(wikiRepo, "master", entry.Name())
c, err := wikiRepo.GetCommitByPath(entry.Name())
if err != nil {
ctx.ServerError("GetCommitByPath", err)
ctx.Error(http.StatusInternalServerError, "GetCommit", err)
return
}
wikiName, err := wiki_service.FilenameToName(entry.Name())
@ -59,157 +408,128 @@ func ListWikiPages(ctx *context.APIContext) {
if repo_model.IsErrWikiInvalidFileName(err) {
continue
}
ctx.ServerError("wiki_service.FilenameToName", err)
ctx.Error(http.StatusInternalServerError, "WikiFilenameToName", err)
return
}
pages = append(pages, hat_api.WikiesResponse{
WikiCloneLink: hat_api.CloneLink{
HTTPS: wikiCloneWiki.HTTPS,
SSH: wikiCloneWiki.SSH,
},
WikiMeta: hat_api.WikiMeta{
Name: wikiName,
Commit: hat_api.WikiCommit{
Author: hat_api.WikiUser{
Name: lastCommit.Author.Name,
Email: lastCommit.Author.Email,
When: lastCommit.Author.When.Unix(),
},
Commiter: hat_api.WikiUser{
Name: lastCommit.Committer.Name,
Email: lastCommit.Committer.Email,
When: lastCommit.Author.When.Unix(),
},
ID: lastCommit.ID.String(),
Message: lastCommit.Message(),
},
FirstCommit: hat_api.WikiCommit{
Author: hat_api.WikiUser{
Name: firstCommit.Author.Name,
Email: firstCommit.Author.Email,
When: firstCommit.Author.When.Unix(),
},
Commiter: hat_api.WikiUser{
Name: firstCommit.Committer.Name,
Email: firstCommit.Committer.Email,
When: firstCommit.Author.When.Unix(),
},
ID: firstCommit.ID.String(),
Message: firstCommit.Message(),
},
},
})
pages = append(pages, convert.ToWikiPageMetaData(wikiName, c, ctx.Repo.Repository))
}
sort.Slice(pages, func(i, j int) bool {
return pages[i].FirstCommit.Author.When > pages[j].FirstCommit.Author.When
})
ctx.SetTotalCountHeader(int64(len(entries)))
ctx.JSON(http.StatusOK, pages)
}
func findWikiRepoCommit(ctx *context.Context) (*git.Repository, *git.Commit, error) {
wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil, nil, err
}
// GetWikiPage get single wiki page
func GetWikiPage(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/page/{pageName} repository repoGetWikiPage
// ---
// summary: Get a wiki page
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// responses:
// "200":
// "$ref": "#/responses/WikiPage"
// "404":
// "$ref": "#/responses/notFound"
commit, err := wikiRepo.GetBranchCommit("master")
if err != nil {
return wikiRepo, nil, err
// get requested pagename
pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName"))
//todo:fix path
wikiPage := getWikiPage(ctx, pageName, "")
if !ctx.Written() {
ctx.JSON(http.StatusOK, wikiPage)
}
return wikiRepo, commit, nil
}
func CreateWiki(ctx *context.Context) {
form := web.GetForm(ctx).(*hat_api.WikiOption)
// ListPageRevisions renders file revision list of wiki page
func ListPageRevisions(ctx *context.APIContext) {
// swagger:operation GET /repos/{owner}/{repo}/wiki/revisions/{pageName} repository repoGetWikiPageRevisions
// ---
// summary: Get revisions of a wiki page
// produces:
// - application/json
// parameters:
// - name: owner
// in: path
// description: owner of the repo
// type: string
// required: true
// - name: repo
// in: path
// description: name of the repo
// type: string
// required: true
// - name: pageName
// in: path
// description: name of the page
// type: string
// required: true
// - name: page
// in: query
// description: page number of results to return (1-based)
// type: integer
// responses:
// "200":
// "$ref": "#/responses/WikiCommitList"
// "404":
// "$ref": "#/responses/notFound"
// wikiName := wiki_service.NormalizeWikiName(form.Name)
wikiName := form.Name
wikiCloneLink := ctx.Repo.Repository.WikiCloneLink()
if err := wiki_service.AddWikiPage(ctx, ctx.Doer, ctx.Repo.Repository, wikiName, form.Content, form.CommitMessage); err != nil {
if repo_model.IsErrWikiReservedName(err) {
ctx.Error(http.StatusInternalServerError, "WikiNameIsReservedPage", "wiki名称是被保留的.")
} else if repo_model.IsErrWikiAlreadyExist(err) {
ctx.Error(http.StatusConflict, "WikiNameAlreadyExist", "wiki名称已存在")
} else {
ctx.Error(http.StatusInternalServerError, "AddWikiPage", err.Error())
}
wikiRepo, commit := findWikiRepoCommit(ctx)
if wikiRepo != nil {
defer wikiRepo.Close()
}
if ctx.Written() {
return
}
wikiRepo, commit, _ := findWikiRepoCommit(ctx)
data, entry, pageFilename := wikiContentsByName(ctx, commit, wikiName)
metas := ctx.Repo.Repository.ComposeDocumentMetas()
var rctx = &markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: metas,
IsWiki: true,
// get requested pagename
pageName := wiki_service.NormalizeWikiName(ctx.Params(":pageName"))
if len(pageName) == 0 {
pageName = "Home"
}
var buf strings.Builder
if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
if wikiRepo != nil {
wikiRepo.Close()
}
ctx.ServerError("Render", err)
//todo:fix path
// lookup filename in wiki - get filecontent, gitTree entry , real filename
_, pageFilename := wikiContentsByName(ctx, commit, pageName, "", false)
if ctx.Written() {
return
}
// get commit count - wiki revisions
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
c, err := wikiRepo.GetCommitByPath(entry.Name())
page := ctx.FormInt("page")
if page <= 1 {
page = 1
}
// get Commit Count
commitsHistory, err := wikiRepo.CommitsByFileAndRange("master", pageFilename, page)
if err != nil {
if repo_model.IsErrWikiInvalidFileName(err) {
return
}
ctx.Error(http.StatusInternalServerError, "CommitsByFileAndRange", err)
return
}
wiki := hat_api.WikiResponse{
WikiCloneLink: hat_api.CloneLink{
HTTPS: wikiCloneLink.HTTPS,
SSH: wikiCloneLink.SSH,
},
WikiMeta: hat_api.WikiMeta{
Name: form.Name,
Commit: hat_api.WikiCommit{
Author: hat_api.WikiUser{
Name: c.Author.Name,
Email: c.Author.Email,
When: c.Author.When.Unix(),
},
Commiter: hat_api.WikiUser{
Name: c.Committer.Name,
Email: c.Committer.Email,
When: c.Author.When.Unix(),
},
ID: c.ID.String(),
Message: c.Message(),
},
},
CommitCounts: commitsCount,
MdContent: string(data),
SimpleContent: buf.String(),
}
ctx.JSON(http.StatusOK, wiki)
}
func wikiContentsByEntry(ctx *context.Context, entry *git.TreeEntry) []byte {
reader, err := entry.Blob().DataAsync()
if err != nil {
ctx.ServerError("Blob.Data", err)
return nil
}
defer reader.Close()
content, err := io.ReadAll(reader)
if err != nil {
ctx.ServerError("ReadAll", err)
return nil
}
return content
ctx.SetTotalCountHeader(commitsCount)
ctx.JSON(http.StatusOK, convert.ToWikiCommitList(commitsHistory, commitsCount))
}
// findEntryForFile finds the tree entry for a target filepath.
func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error) {
entry, err := commit.GetTreeEntryByPath(target)
if err != nil {
@ -227,184 +547,76 @@ func findEntryForFile(commit *git.Commit, target string) (*git.TreeEntry, error)
return commit.GetTreeEntryByPath(unescapedTarget)
}
// findWikiRepoCommit opens the wiki repo and returns the latest commit, writing to context on error.
// The caller is responsible for closing the returned repo again
func findWikiRepoCommit(ctx *context.APIContext) (*git.Repository, *git.Commit) {
wikiRepo, err := git.OpenRepository(ctx, ctx.Repo.Repository.WikiPath())
if err != nil {
if git.IsErrNotExist(err) || err.Error() == "no such file or directory" {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
}
return nil, nil
}
commit, err := wikiRepo.GetBranchCommit("master")
if err != nil {
if git.IsErrNotExist(err) {
ctx.NotFound(err)
} else {
ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
}
return wikiRepo, nil
}
return wikiRepo, commit
}
// wikiContentsByEntry returns the contents of the wiki page referenced by the
// given tree entry, encoded with base64. Writes to ctx if an error occurs.
func wikiContentsByEntry(ctx *context.APIContext, entry *git.TreeEntry) string {
blob := entry.Blob()
if blob.Size() > setting.API.DefaultMaxBlobSize {
return ""
}
content, err := blob.GetBlobContentBase64()
if err != nil {
ctx.Error(http.StatusInternalServerError, "GetBlobContentBase64", err)
return ""
}
return content
}
// 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) {
pageFilename := wiki_service.NameToFilename(wikiName)
func wikiContentsByName(ctx *context.APIContext, commit *git.Commit, wikiName, path string, isSidebarOrFooter bool) (string, string) {
pageFilename := wiki_service.NameToFilename(wikiName, path)
entry, err := findEntryForFile(commit, pageFilename)
if err != nil {
if git.IsErrNotExist(err) {
if !isSidebarOrFooter {
ctx.NotFound()
}
} else {
ctx.ServerError("findEntryForFile", err)
}
return nil, nil, ""
return "", ""
}
return wikiContentsByEntry(ctx, entry), entry, pageFilename
return wikiContentsByEntry(ctx, entry), pageFilename
}
func GetWiki(ctx *context.APIContext) {
wikiRepo, commit, _ := findWikiRepoCommit(ctx.Context)
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
wikiCloneWiki := ctx.Repo.Repository.WikiCloneLink()
// pageName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
pageName := ctx.Params(":page")
if len(pageName) == 0 {
pageName = "Home"
}
data, entry, pageFileName := wikiContentsByName(ctx.Context, commit, pageName)
if entry == nil {
ctx.NotFound()
return
}
metas := ctx.Repo.Repository.ComposeDocumentMetas()
var rctx = &markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: metas,
IsWiki: true,
}
var buf strings.Builder
content := blackfriday.Run(data)
if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
ctx.ServerError("markdown.Render", err)
return
}
c, err := wikiRepo.GetCommitByPath(entry.Name())
func wikiDirByName(ctx *context.APIContext, commit *git.Commit, dirName, path string, isSidebarOrFooter bool) (string, string) {
pageFilename := wiki_service.NameToDirName(dirName, path)
entry, err := findEntryForFile(commit, pageFilename)
if err != nil {
if repo_model.IsErrWikiInvalidFileName(err) {
return
if git.IsErrNotExist(err) {
if !isSidebarOrFooter {
ctx.NotFound()
}
} else {
ctx.ServerError("findEntryForFile", err)
}
return "", ""
}
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFileName)
wiki := hat_api.WikiResponse{
WikiCloneLink: hat_api.CloneLink{
HTTPS: wikiCloneWiki.HTTPS,
SSH: wikiCloneWiki.SSH,
},
WikiMeta: hat_api.WikiMeta{
Name: pageName,
Commit: hat_api.WikiCommit{
Author: hat_api.WikiUser{
Name: c.Author.Name,
Email: c.Author.Email,
When: c.Author.When.Unix(),
},
Commiter: hat_api.WikiUser{
Name: c.Committer.Name,
Email: c.Committer.Email,
When: c.Committer.When.Unix(),
},
},
},
CommitCounts: commitsCount,
MdContent: string(data),
SimpleContent: string(content),
}
ctx.JSON(http.StatusOK, wiki)
}
func EditWiki(ctx *context.APIContext) {
form := web.GetForm(ctx).(*hat_api.WikiOption)
// oldWikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
oldWikiName := ctx.Params(":page")
// newWikiName := wiki_service.NormalizeWikiName(form.Name)
newWikiName := form.Name
wikiRepo, commit, _ := findWikiRepoCommit(ctx.Context)
defer func() {
if wikiRepo != nil {
wikiRepo.Close()
}
}()
if _, entry, _ := wikiContentsByName(ctx.Context, commit, oldWikiName); entry == nil {
ctx.Error(http.StatusNotFound, "WikiNotFound", "Wiki不存在")
return
}
if _, entry, _ := wikiContentsByName(ctx.Context, commit, newWikiName); oldWikiName != newWikiName && entry != nil {
ctx.Error(http.StatusNotFound, "WikiNameAlreadyExist", "Wiki已存在")
return
}
if len(form.CommitMessage) == 0 {
form.CommitMessage = ctx.Tr("repo.editor.update", form.Name)
}
if err := wiki_service.EditWikiPage(ctx.Context, ctx.Doer, ctx.Repo.Repository, oldWikiName, newWikiName, form.Content, form.CommitMessage); err != nil {
ctx.Error(http.StatusInternalServerError, "EditWikiPage", err)
return
}
_, newCommit, _ := findWikiRepoCommit(ctx.Context)
data, entry, pageFilename := wikiContentsByName(ctx.Context, newCommit, newWikiName)
c, err := wikiRepo.GetCommitByPath(entry.Name())
if err != nil {
if repo_model.IsErrWikiInvalidFileName(err) {
return
}
}
metas := ctx.Repo.Repository.ComposeDocumentMetas()
var rctx = &markup.RenderContext{
URLPrefix: ctx.Repo.RepoLink,
Metas: metas,
IsWiki: true,
}
var buf strings.Builder
if err := markdown.Render(rctx, bytes.NewReader(data), &buf); err != nil {
ctx.ServerError("markdown.Render", err)
return
}
commitsCount, _ := wikiRepo.FileCommitsCount("master", pageFilename)
wiki := hat_api.WikiResponse{
WikiMeta: hat_api.WikiMeta{
Name: form.Name,
Commit: hat_api.WikiCommit{
Author: hat_api.WikiUser{
Name: c.Author.Name,
Email: c.Author.Email,
When: c.Author.When.Unix(),
},
Commiter: hat_api.WikiUser{
Name: c.Committer.Name,
Email: c.Committer.Email,
When: c.Author.When.Unix(),
},
ID: c.ID.String(),
Message: c.Message(),
},
},
CommitCounts: commitsCount,
MdContent: string(data),
SimpleContent: buf.String(),
}
ctx.JSON(http.StatusOK, wiki)
}
func DeleteWiki(ctx *context.APIContext) {
// wikiName := wiki_service.NormalizeWikiName(ctx.Params(":page"))
wikiName := ctx.Params(":page")
if len(wikiName) == 0 {
wikiName = "Home"
}
err := wiki_service.DeleteWikiPage(ctx.Context, ctx.Doer, ctx.Repo.Repository, wikiName)
if err != nil {
ctx.ServerError("wiki_service.DeleteWikiPage", err)
return
}
ctx.Status(http.StatusNoContent)
return wikiContentsByEntry(ctx, entry), pageFilename
}

437
services/wiki/wiki.go Normal file
View File

@ -0,0 +1,437 @@
// 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 (
"context"
"fmt"
"net/url"
"os"
"strings"
repo_model "code.gitea.io/gitea/models/repo"
system_model "code.gitea.io/gitea/models/system"
"code.gitea.io/gitea/models/unit"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"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()
)
const (
DefaultRemote = "origin"
DefaultBranch = "master"
)
func nameAllowed(name string) error {
if util.IsStringInSlice(name, reservedWikiNames) {
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, path string) string {
name = strings.ReplaceAll(name, " ", "-")
return path + url.QueryEscape(name) + ".md"
}
// NameToDirName converts a wiki name to its corresponding filename.
func NameToDirName(name, path string) string {
name = strings.ReplaceAll(name, " ", "-")
return path + url.QueryEscape(name) + "/.keep"
}
// 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 {
if repo.HasWiki() {
return nil
}
if err := git.InitRepository(ctx, repo.WikiPath(), true); err != nil {
return fmt.Errorf("InitRepository: %w", err)
} else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
return fmt.Errorf("createDelegateHooks: %w", err)
} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD", git.BranchPrefix+DefaultBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
return fmt.Errorf("unable to set default wiki branch to master: %w", err)
}
return nil
}
// prepareWikiFileName 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, path string) (bool, string, error) {
unescaped := path + wikiName + ".md"
escaped := NameToFilename(wikiName, path)
// Look for both files
filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, escaped)
if err != nil {
if strings.Contains(err.Error(), "Not a valid object name master") {
return false, escaped, nil
}
log.Error("%v", err)
return false, escaped, err
}
foundEscaped := false
for _, filename := range filesInIndex {
switch filename {
case unescaped:
// if we find the unescaped file return it
return true, unescaped, nil
case escaped:
foundEscaped = true
}
}
// If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
return foundEscaped, escaped, nil
}
// prepareWikiDirName 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 prepareWikiDirName(gitRepo *git.Repository, wikiName, path string) (bool, string, error) {
unescaped := path + wikiName + "/.keep"
escaped := NameToDirName(wikiName, path)
// Look for both files
filesInIndex, err := gitRepo.LsTree(DefaultBranch, unescaped, escaped)
if err != nil {
if strings.Contains(err.Error(), "Not a valid object name master") {
return false, escaped, nil
}
log.Error("%v", err)
return false, escaped, err
}
foundEscaped := false
for _, filename := range filesInIndex {
switch filename {
case unescaped:
// if we find the unescaped file return it
return true, unescaped, nil
case escaped:
foundEscaped = true
}
}
// If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
return foundEscaped, escaped, 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, path, content, message string, isNew bool, isDir bool) (err error) {
if err = nameAllowed(newWikiName); err != nil {
return err
}
wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
if err = InitWiki(ctx, repo); err != nil {
return fmt.Errorf("InitWiki: %w", err)
}
hasMasterBranch := git.IsBranchExist(ctx, repo.WikiPath(), DefaultBranch)
basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil {
return err
}
defer func() {
if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
cloneOpts := git.CloneRepoOptions{
Bare: true,
Shared: true,
}
if hasMasterBranch {
cloneOpts.Branch = DefaultBranch
}
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)
}
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)
}
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)
}
}
var isWikiExist bool
var newWikiPath string
if !isDir {
isWikiExist, newWikiPath, err = prepareWikiFileName(gitRepo, newWikiName, path)
} else {
isWikiExist, newWikiPath, err = prepareWikiDirName(gitRepo, newWikiName, path)
}
if err != nil {
return err
}
if isNew {
if isWikiExist {
return repo_model.ErrWikiAlreadyExist{
Title: newWikiPath,
}
}
} else {
// avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free.
isOldWikiExist := true
oldWikiPath := newWikiPath
if oldWikiName != newWikiName {
isOldWikiExist, oldWikiPath, err = prepareWikiFileName(gitRepo, oldWikiName, path)
if err != nil {
return err
}
}
if isOldWikiExist {
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,
}
committer := doer.NewGitSig()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo.WikiPath(), doer)
if sign {
commitTreeOpts.KeyID = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
committer = signer
}
} else {
commitTreeOpts.NoGPGSign = true
}
if hasMasterBranch {
commitTreeOpts.Parents = []string{"HEAD"}
}
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
if err != nil {
log.Error("%v", err)
return err
}
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
Env: repo_module.FullPushingEnvironment(
doer,
doer,
repo,
repo.Name+".wiki",
0,
),
}); err != nil {
log.Error("%v", err)
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
return fmt.Errorf("Push: %w", err)
}
return nil
}
// AddWikiPage adds a new wiki page with a given wikiPath.
func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName, path, content, message string) error {
return updateWikiPage(ctx, doer, repo, "", wikiName, path, content, message, true, false)
}
// AddWikiDir adds a new wiki page with a given wikiPath.
func AddWikiDir(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName, path, message string) error {
return updateWikiPage(ctx, doer, repo, "", wikiName, path, "", message, true, 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 {
return updateWikiPage(ctx, doer, repo, oldWikiName, newWikiName, "", content, message, false, 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) {
wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
if err = InitWiki(ctx, repo); err != nil {
return fmt.Errorf("InitWiki: %w", err)
}
basePath, err := repo_module.CreateTemporaryPath("update-wiki")
if err != nil {
return err
}
defer func() {
if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
log.Error("Merge: RemoveTemporaryPath: %s", err)
}
}()
if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
Bare: true,
Shared: true,
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)
}
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)
}
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)
}
// todo:fix path
found, wikiPath, err := prepareWikiFileName(gitRepo, wikiName, "")
if err != nil {
return err
}
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"},
}
committer := doer.NewGitSig()
sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo.WikiPath(), doer)
if sign {
commitTreeOpts.KeyID = signingKey
if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
committer = signer
}
} else {
commitTreeOpts.NoGPGSign = true
}
commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
if err != nil {
return err
}
if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
Remote: DefaultRemote,
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, DefaultBranch),
Env: repo_module.PushingEnvironment(doer, repo),
}); err != nil {
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
return err
}
return fmt.Errorf("Push: %w", err)
}
return nil
}
// DeleteWiki removes the actual and local copy of repository wiki.
func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
if err := repo_model.UpdateRepositoryUnits(repo, nil, []unit.Type{unit.TypeWiki}); err != nil {
return err
}
system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
return nil
}