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

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