You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

wiki.go 10KB

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