gitea源码

wiki.go 12KB

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