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 12KB

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