mirror of
https://github.com/go-gitea/gitea
synced 2024-11-14 14:14:25 +00:00
4979f15c3f
* Add configurable Trust Models Gitea's default signature verification model differs from GitHub. GitHub uses signatures to verify that the committer is who they say they are - meaning that when GitHub makes a signed commit it must be the committer. The GitHub model prevents re-publishing of commits after revocation of a key and prevents re-signing of other people's commits to create a completely trusted repository signed by one key or a set of trusted keys. The default behaviour of Gitea in contrast is to always display the avatar and information related to a signature. This allows signatures to be decoupled from the committer. That being said, allowing arbitary users to present other peoples commits as theirs is not necessarily desired therefore we have a trust model whereby signatures from collaborators are marked trusted, signatures matching the commit line are marked untrusted and signatures that match a user in the db but not the committer line are marked unmatched. The problem with this model is that this conflicts with Github therefore we need to provide an option to allow users to choose the Github model should they wish to. Signed-off-by: Andrew Thornton <art27@cantab.net> * Adjust locale strings Signed-off-by: Andrew Thornton <art27@cantab.net> * as per @6543 Co-authored-by: 6543 <6543@obermui.de> * Update models/gpg_key.go * Add migration for repository Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
341 lines
9.0 KiB
Go
341 lines
9.0 KiB
Go
// 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"
|
|
repo_module "code.gitea.io/gitea/modules/repository"
|
|
"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 = repo_module.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,
|
|
}
|
|
|
|
committer := doer.NewGitSig()
|
|
|
|
sign, signingKey, signer, _ := repo.SignWikiCommit(doer)
|
|
if sign {
|
|
commitTreeOpts.KeyID = signingKey
|
|
if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.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(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)
|
|
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
|
return 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"},
|
|
}
|
|
|
|
committer := doer.NewGitSig()
|
|
|
|
sign, signingKey, signer, _ := repo.SignWikiCommit(doer)
|
|
if sign {
|
|
commitTreeOpts.KeyID = signingKey
|
|
if repo.GetTrustModel() == models.CommitterTrustModel || repo.GetTrustModel() == models.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(basePath, git.PushOptions{
|
|
Remote: "origin",
|
|
Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
|
|
Env: models.PushingEnvironment(doer, repo),
|
|
}); err != nil {
|
|
if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
|
|
return err
|
|
}
|
|
return fmt.Errorf("Push: %v", err)
|
|
}
|
|
|
|
return nil
|
|
}
|