code.gitea.io/gitea@v1.22.3/services/wiki/wiki.go (about)

     1  // Copyright 2015 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package wiki
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  	"os"
    12  	"strings"
    13  
    14  	"code.gitea.io/gitea/models/db"
    15  	repo_model "code.gitea.io/gitea/models/repo"
    16  	system_model "code.gitea.io/gitea/models/system"
    17  	"code.gitea.io/gitea/models/unit"
    18  	user_model "code.gitea.io/gitea/models/user"
    19  	"code.gitea.io/gitea/modules/git"
    20  	"code.gitea.io/gitea/modules/gitrepo"
    21  	"code.gitea.io/gitea/modules/log"
    22  	repo_module "code.gitea.io/gitea/modules/repository"
    23  	"code.gitea.io/gitea/modules/sync"
    24  	"code.gitea.io/gitea/modules/util"
    25  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    26  	repo_service "code.gitea.io/gitea/services/repository"
    27  )
    28  
    29  // TODO: use clustered lock (unique queue? or *abuse* cache)
    30  var wikiWorkingPool = sync.NewExclusivePool()
    31  
    32  const DefaultRemote = "origin"
    33  
    34  // InitWiki initializes a wiki for repository,
    35  // it does nothing when repository already has wiki.
    36  func InitWiki(ctx context.Context, repo *repo_model.Repository) error {
    37  	if repo.HasWiki() {
    38  		return nil
    39  	}
    40  
    41  	if err := git.InitRepository(ctx, repo.WikiPath(), true, repo.ObjectFormatName); err != nil {
    42  		return fmt.Errorf("InitRepository: %w", err)
    43  	} else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
    44  		return fmt.Errorf("createDelegateHooks: %w", err)
    45  	} else if _, _, err = git.NewCommand(ctx, "symbolic-ref", "HEAD").AddDynamicArguments(git.BranchPrefix + repo.DefaultWikiBranch).RunStdString(&git.RunOpts{Dir: repo.WikiPath()}); err != nil {
    46  		return fmt.Errorf("unable to set default wiki branch to %q: %w", repo.DefaultWikiBranch, err)
    47  	}
    48  	return nil
    49  }
    50  
    51  // prepareGitPath try to find a suitable file path with file name by the given raw wiki name.
    52  // return: existence, prepared file path with name, error
    53  func prepareGitPath(gitRepo *git.Repository, defaultWikiBranch string, wikiPath WebPath) (bool, string, error) {
    54  	unescaped := string(wikiPath) + ".md"
    55  	gitPath := WebPathToGitPath(wikiPath)
    56  
    57  	// Look for both files
    58  	filesInIndex, err := gitRepo.LsTree(defaultWikiBranch, unescaped, gitPath)
    59  	if err != nil {
    60  		if strings.Contains(err.Error(), "Not a valid object name") {
    61  			return false, gitPath, nil // branch doesn't exist
    62  		}
    63  		log.Error("Wiki LsTree failed, err: %v", err)
    64  		return false, gitPath, err
    65  	}
    66  
    67  	foundEscaped := false
    68  	for _, filename := range filesInIndex {
    69  		switch filename {
    70  		case unescaped:
    71  			// if we find the unescaped file return it
    72  			return true, unescaped, nil
    73  		case gitPath:
    74  			foundEscaped = true
    75  		}
    76  	}
    77  
    78  	// If not return whether the escaped file exists, and the escaped filename to keep backwards compatibility.
    79  	return foundEscaped, gitPath, nil
    80  }
    81  
    82  // updateWikiPage adds a new page or edits an existing page in repository wiki.
    83  func updateWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string, isNew bool) (err error) {
    84  	err = repo.MustNotBeArchived()
    85  	if err != nil {
    86  		return err
    87  	}
    88  
    89  	if err = validateWebPath(newWikiName); err != nil {
    90  		return err
    91  	}
    92  	wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
    93  	defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
    94  
    95  	if err = InitWiki(ctx, repo); err != nil {
    96  		return fmt.Errorf("InitWiki: %w", err)
    97  	}
    98  
    99  	hasDefaultBranch := git.IsBranchExist(ctx, repo.WikiPath(), repo.DefaultWikiBranch)
   100  
   101  	basePath, err := repo_module.CreateTemporaryPath("update-wiki")
   102  	if err != nil {
   103  		return err
   104  	}
   105  	defer func() {
   106  		if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
   107  			log.Error("Merge: RemoveTemporaryPath: %s", err)
   108  		}
   109  	}()
   110  
   111  	cloneOpts := git.CloneRepoOptions{
   112  		Bare:   true,
   113  		Shared: true,
   114  	}
   115  
   116  	if hasDefaultBranch {
   117  		cloneOpts.Branch = repo.DefaultWikiBranch
   118  	}
   119  
   120  	if err := git.Clone(ctx, repo.WikiPath(), basePath, cloneOpts); err != nil {
   121  		log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
   122  		return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
   123  	}
   124  
   125  	gitRepo, err := git.OpenRepository(ctx, basePath)
   126  	if err != nil {
   127  		log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
   128  		return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err)
   129  	}
   130  	defer gitRepo.Close()
   131  
   132  	if hasDefaultBranch {
   133  		if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
   134  			log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
   135  			return fmt.Errorf("fnable to read HEAD tree to index in: %s %w", basePath, err)
   136  		}
   137  	}
   138  
   139  	isWikiExist, newWikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, newWikiName)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	if isNew {
   145  		if isWikiExist {
   146  			return repo_model.ErrWikiAlreadyExist{
   147  				Title: newWikiPath,
   148  			}
   149  		}
   150  	} else {
   151  		// avoid check existence again if wiki name is not changed since gitRepo.LsFiles(...) is not free.
   152  		isOldWikiExist := true
   153  		oldWikiPath := newWikiPath
   154  		if oldWikiName != newWikiName {
   155  			isOldWikiExist, oldWikiPath, err = prepareGitPath(gitRepo, repo.DefaultWikiBranch, oldWikiName)
   156  			if err != nil {
   157  				return err
   158  			}
   159  		}
   160  
   161  		if isOldWikiExist {
   162  			err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
   163  			if err != nil {
   164  				log.Error("RemoveFilesFromIndex failed: %v", err)
   165  				return err
   166  			}
   167  		}
   168  	}
   169  
   170  	// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
   171  
   172  	objectHash, err := gitRepo.HashObject(strings.NewReader(content))
   173  	if err != nil {
   174  		log.Error("HashObject failed: %v", err)
   175  		return err
   176  	}
   177  
   178  	if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
   179  		log.Error("AddObjectToIndex failed: %v", err)
   180  		return err
   181  	}
   182  
   183  	tree, err := gitRepo.WriteTree()
   184  	if err != nil {
   185  		log.Error("WriteTree failed: %v", err)
   186  		return err
   187  	}
   188  
   189  	commitTreeOpts := git.CommitTreeOpts{
   190  		Message: message,
   191  	}
   192  
   193  	committer := doer.NewGitSig()
   194  
   195  	sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
   196  	if sign {
   197  		commitTreeOpts.KeyID = signingKey
   198  		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
   199  			committer = signer
   200  		}
   201  	} else {
   202  		commitTreeOpts.NoGPGSign = true
   203  	}
   204  	if hasDefaultBranch {
   205  		commitTreeOpts.Parents = []string{"HEAD"}
   206  	}
   207  
   208  	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
   209  	if err != nil {
   210  		log.Error("CommitTree failed: %v", err)
   211  		return err
   212  	}
   213  
   214  	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
   215  		Remote: DefaultRemote,
   216  		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
   217  		Env: repo_module.FullPushingEnvironment(
   218  			doer,
   219  			doer,
   220  			repo,
   221  			repo.Name+".wiki",
   222  			0,
   223  		),
   224  	}); err != nil {
   225  		log.Error("Push failed: %v", err)
   226  		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
   227  			return err
   228  		}
   229  		return fmt.Errorf("failed to push: %w", err)
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  // AddWikiPage adds a new wiki page with a given wikiPath.
   236  func AddWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath, content, message string) error {
   237  	return updateWikiPage(ctx, doer, repo, "", wikiName, content, message, true)
   238  }
   239  
   240  // EditWikiPage updates a wiki page identified by its wikiPath,
   241  // optionally also changing wikiPath.
   242  func EditWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldWikiName, newWikiName WebPath, content, message string) error {
   243  	return updateWikiPage(ctx, doer, repo, oldWikiName, newWikiName, content, message, false)
   244  }
   245  
   246  // DeleteWikiPage deletes a wiki page identified by its path.
   247  func DeleteWikiPage(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, wikiName WebPath) (err error) {
   248  	err = repo.MustNotBeArchived()
   249  	if err != nil {
   250  		return err
   251  	}
   252  
   253  	wikiWorkingPool.CheckIn(fmt.Sprint(repo.ID))
   254  	defer wikiWorkingPool.CheckOut(fmt.Sprint(repo.ID))
   255  
   256  	if err = InitWiki(ctx, repo); err != nil {
   257  		return fmt.Errorf("InitWiki: %w", err)
   258  	}
   259  
   260  	basePath, err := repo_module.CreateTemporaryPath("update-wiki")
   261  	if err != nil {
   262  		return err
   263  	}
   264  	defer func() {
   265  		if err := repo_module.RemoveTemporaryPath(basePath); err != nil {
   266  			log.Error("Merge: RemoveTemporaryPath: %s", err)
   267  		}
   268  	}()
   269  
   270  	if err := git.Clone(ctx, repo.WikiPath(), basePath, git.CloneRepoOptions{
   271  		Bare:   true,
   272  		Shared: true,
   273  		Branch: repo.DefaultWikiBranch,
   274  	}); err != nil {
   275  		log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
   276  		return fmt.Errorf("failed to clone repository: %s (%w)", repo.FullName(), err)
   277  	}
   278  
   279  	gitRepo, err := git.OpenRepository(ctx, basePath)
   280  	if err != nil {
   281  		log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
   282  		return fmt.Errorf("failed to open new temporary repository in: %s %w", basePath, err)
   283  	}
   284  	defer gitRepo.Close()
   285  
   286  	if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
   287  		log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
   288  		return fmt.Errorf("unable to read HEAD tree to index in: %s %w", basePath, err)
   289  	}
   290  
   291  	found, wikiPath, err := prepareGitPath(gitRepo, repo.DefaultWikiBranch, wikiName)
   292  	if err != nil {
   293  		return err
   294  	}
   295  	if found {
   296  		err := gitRepo.RemoveFilesFromIndex(wikiPath)
   297  		if err != nil {
   298  			return err
   299  		}
   300  	} else {
   301  		return os.ErrNotExist
   302  	}
   303  
   304  	// FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
   305  
   306  	tree, err := gitRepo.WriteTree()
   307  	if err != nil {
   308  		return err
   309  	}
   310  	message := fmt.Sprintf("Delete page %q", wikiName)
   311  	commitTreeOpts := git.CommitTreeOpts{
   312  		Message: message,
   313  		Parents: []string{"HEAD"},
   314  	}
   315  
   316  	committer := doer.NewGitSig()
   317  
   318  	sign, signingKey, signer, _ := asymkey_service.SignWikiCommit(ctx, repo, doer)
   319  	if sign {
   320  		commitTreeOpts.KeyID = signingKey
   321  		if repo.GetTrustModel() == repo_model.CommitterTrustModel || repo.GetTrustModel() == repo_model.CollaboratorCommitterTrustModel {
   322  			committer = signer
   323  		}
   324  	} else {
   325  		commitTreeOpts.NoGPGSign = true
   326  	}
   327  
   328  	commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), committer, tree, commitTreeOpts)
   329  	if err != nil {
   330  		return err
   331  	}
   332  
   333  	if err := git.Push(gitRepo.Ctx, basePath, git.PushOptions{
   334  		Remote: DefaultRemote,
   335  		Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, repo.DefaultWikiBranch),
   336  		Env: repo_module.FullPushingEnvironment(
   337  			doer,
   338  			doer,
   339  			repo,
   340  			repo.Name+".wiki",
   341  			0,
   342  		),
   343  	}); err != nil {
   344  		if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
   345  			return err
   346  		}
   347  		return fmt.Errorf("Push: %w", err)
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  // DeleteWiki removes the actual and local copy of repository wiki.
   354  func DeleteWiki(ctx context.Context, repo *repo_model.Repository) error {
   355  	if err := repo_service.UpdateRepositoryUnits(ctx, repo, nil, []unit.Type{unit.TypeWiki}); err != nil {
   356  		return err
   357  	}
   358  
   359  	system_model.RemoveAllWithNotice(ctx, "Delete repository wiki", repo.WikiPath())
   360  	return nil
   361  }
   362  
   363  func ChangeDefaultWikiBranch(ctx context.Context, repo *repo_model.Repository, newBranch string) error {
   364  	if !git.IsValidRefPattern(newBranch) {
   365  		return fmt.Errorf("invalid branch name: %s", newBranch)
   366  	}
   367  	return db.WithTx(ctx, func(ctx context.Context) error {
   368  		repo.DefaultWikiBranch = newBranch
   369  		if err := repo_model.UpdateRepositoryCols(ctx, repo, "default_wiki_branch"); err != nil {
   370  			return fmt.Errorf("unable to update database: %w", err)
   371  		}
   372  
   373  		if !repo.HasWiki() {
   374  			return nil
   375  		}
   376  
   377  		oldDefBranch, err := gitrepo.GetWikiDefaultBranch(ctx, repo)
   378  		if err != nil {
   379  			return fmt.Errorf("unable to get default branch: %w", err)
   380  		}
   381  		if oldDefBranch == newBranch {
   382  			return nil
   383  		}
   384  
   385  		gitRepo, err := gitrepo.OpenWikiRepository(ctx, repo)
   386  		if errors.Is(err, util.ErrNotExist) {
   387  			return nil // no git repo on storage, no need to do anything else
   388  		} else if err != nil {
   389  			return fmt.Errorf("unable to open repository: %w", err)
   390  		}
   391  		defer gitRepo.Close()
   392  
   393  		err = gitRepo.RenameBranch(oldDefBranch, newBranch)
   394  		if err != nil {
   395  			return fmt.Errorf("unable to rename default branch: %w", err)
   396  		}
   397  		return nil
   398  	})
   399  }