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.

329 lines
8.7 KiB

  1. // Copyright 2015 The Gogs Authors. All rights reserved.
  2. // Copyright 2019 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package wiki
  6. import (
  7. "fmt"
  8. "net/url"
  9. "os"
  10. "strings"
  11. "code.gitea.io/gitea/models"
  12. "code.gitea.io/gitea/modules/git"
  13. "code.gitea.io/gitea/modules/log"
  14. repo_module "code.gitea.io/gitea/modules/repository"
  15. "code.gitea.io/gitea/modules/sync"
  16. "code.gitea.io/gitea/modules/util"
  17. "github.com/unknwon/com"
  18. )
  19. var (
  20. reservedWikiNames = []string{"_pages", "_new", "_edit", "raw"}
  21. wikiWorkingPool = sync.NewExclusivePool()
  22. )
  23. func nameAllowed(name string) error {
  24. if util.IsStringInSlice(name, reservedWikiNames) {
  25. return models.ErrWikiReservedName{
  26. Title: name,
  27. }
  28. }
  29. return nil
  30. }
  31. // NameToSubURL converts a wiki name to its corresponding sub-URL.
  32. func NameToSubURL(name string) string {
  33. return url.QueryEscape(strings.Replace(name, " ", "-", -1))
  34. }
  35. // NormalizeWikiName normalizes a wiki name
  36. func NormalizeWikiName(name string) string {
  37. return strings.Replace(name, "-", " ", -1)
  38. }
  39. // NameToFilename converts a wiki name to its corresponding filename.
  40. func NameToFilename(name string) string {
  41. name = strings.Replace(name, " ", "-", -1)
  42. return url.QueryEscape(name) + ".md"
  43. }
  44. // FilenameToName converts a wiki filename to its corresponding page name.
  45. func FilenameToName(filename string) (string, error) {
  46. if !strings.HasSuffix(filename, ".md") {
  47. return "", models.ErrWikiInvalidFileName{
  48. FileName: filename,
  49. }
  50. }
  51. basename := filename[:len(filename)-3]
  52. unescaped, err := url.QueryUnescape(basename)
  53. if err != nil {
  54. return "", err
  55. }
  56. return NormalizeWikiName(unescaped), nil
  57. }
  58. // InitWiki initializes a wiki for repository,
  59. // it does nothing when repository already has wiki.
  60. func InitWiki(repo *models.Repository) error {
  61. if repo.HasWiki() {
  62. return nil
  63. }
  64. if err := git.InitRepository(repo.WikiPath(), true); err != nil {
  65. return fmt.Errorf("InitRepository: %v", err)
  66. } else if err = repo_module.CreateDelegateHooks(repo.WikiPath()); err != nil {
  67. return fmt.Errorf("createDelegateHooks: %v", err)
  68. }
  69. return nil
  70. }
  71. // updateWikiPage adds a new page to the repository wiki.
  72. func updateWikiPage(doer *models.User, repo *models.Repository, oldWikiName, newWikiName, content, message string, isNew bool) (err error) {
  73. if err = nameAllowed(newWikiName); err != nil {
  74. return err
  75. }
  76. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  77. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  78. if err = InitWiki(repo); err != nil {
  79. return fmt.Errorf("InitWiki: %v", err)
  80. }
  81. hasMasterBranch := git.IsBranchExist(repo.WikiPath(), "master")
  82. basePath, err := models.CreateTemporaryPath("update-wiki")
  83. if err != nil {
  84. return err
  85. }
  86. defer func() {
  87. if err := models.RemoveTemporaryPath(basePath); err != nil {
  88. log.Error("Merge: RemoveTemporaryPath: %s", err)
  89. }
  90. }()
  91. cloneOpts := git.CloneRepoOptions{
  92. Bare: true,
  93. Shared: true,
  94. }
  95. if hasMasterBranch {
  96. cloneOpts.Branch = "master"
  97. }
  98. if err := git.Clone(repo.WikiPath(), basePath, cloneOpts); err != nil {
  99. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  100. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  101. }
  102. gitRepo, err := git.OpenRepository(basePath)
  103. if err != nil {
  104. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  105. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  106. }
  107. defer gitRepo.Close()
  108. if hasMasterBranch {
  109. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  110. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  111. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  112. }
  113. }
  114. newWikiPath := NameToFilename(newWikiName)
  115. if isNew {
  116. filesInIndex, err := gitRepo.LsFiles(newWikiPath)
  117. if err != nil {
  118. log.Error("%v", err)
  119. return err
  120. }
  121. if util.IsStringInSlice(newWikiPath, filesInIndex) {
  122. return models.ErrWikiAlreadyExist{
  123. Title: newWikiPath,
  124. }
  125. }
  126. } else {
  127. oldWikiPath := NameToFilename(oldWikiName)
  128. filesInIndex, err := gitRepo.LsFiles(oldWikiPath)
  129. if err != nil {
  130. log.Error("%v", err)
  131. return err
  132. }
  133. if util.IsStringInSlice(oldWikiPath, filesInIndex) {
  134. err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
  135. if err != nil {
  136. log.Error("%v", err)
  137. return err
  138. }
  139. }
  140. }
  141. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  142. objectHash, err := gitRepo.HashObject(strings.NewReader(content))
  143. if err != nil {
  144. log.Error("%v", err)
  145. return err
  146. }
  147. if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
  148. log.Error("%v", err)
  149. return err
  150. }
  151. tree, err := gitRepo.WriteTree()
  152. if err != nil {
  153. log.Error("%v", err)
  154. return err
  155. }
  156. commitTreeOpts := git.CommitTreeOpts{
  157. Message: message,
  158. }
  159. sign, signingKey, _ := repo.SignWikiCommit(doer)
  160. if sign {
  161. commitTreeOpts.KeyID = signingKey
  162. } else {
  163. commitTreeOpts.NoGPGSign = true
  164. }
  165. if hasMasterBranch {
  166. commitTreeOpts.Parents = []string{"HEAD"}
  167. }
  168. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  169. if err != nil {
  170. log.Error("%v", err)
  171. return err
  172. }
  173. if err := git.Push(basePath, git.PushOptions{
  174. Remote: "origin",
  175. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  176. Env: models.FullPushingEnvironment(
  177. doer,
  178. doer,
  179. repo,
  180. repo.Name+".wiki",
  181. 0,
  182. ),
  183. }); err != nil {
  184. log.Error("%v", err)
  185. if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
  186. return err
  187. }
  188. return fmt.Errorf("Push: %v", err)
  189. }
  190. return nil
  191. }
  192. // AddWikiPage adds a new wiki page with a given wikiPath.
  193. func AddWikiPage(doer *models.User, repo *models.Repository, wikiName, content, message string) error {
  194. return updateWikiPage(doer, repo, "", wikiName, content, message, true)
  195. }
  196. // EditWikiPage updates a wiki page identified by its wikiPath,
  197. // optionally also changing wikiPath.
  198. func EditWikiPage(doer *models.User, repo *models.Repository, oldWikiName, newWikiName, content, message string) error {
  199. return updateWikiPage(doer, repo, oldWikiName, newWikiName, content, message, false)
  200. }
  201. // DeleteWikiPage deletes a wiki page identified by its path.
  202. func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) (err error) {
  203. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  204. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  205. if err = InitWiki(repo); err != nil {
  206. return fmt.Errorf("InitWiki: %v", err)
  207. }
  208. basePath, err := models.CreateTemporaryPath("update-wiki")
  209. if err != nil {
  210. return err
  211. }
  212. defer func() {
  213. if err := models.RemoveTemporaryPath(basePath); err != nil {
  214. log.Error("Merge: RemoveTemporaryPath: %s", err)
  215. }
  216. }()
  217. if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
  218. Bare: true,
  219. Shared: true,
  220. Branch: "master",
  221. }); err != nil {
  222. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  223. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  224. }
  225. gitRepo, err := git.OpenRepository(basePath)
  226. if err != nil {
  227. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  228. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  229. }
  230. defer gitRepo.Close()
  231. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  232. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  233. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  234. }
  235. wikiPath := NameToFilename(wikiName)
  236. filesInIndex, err := gitRepo.LsFiles(wikiPath)
  237. found := false
  238. for _, file := range filesInIndex {
  239. if file == wikiPath {
  240. found = true
  241. break
  242. }
  243. }
  244. if found {
  245. err := gitRepo.RemoveFilesFromIndex(wikiPath)
  246. if err != nil {
  247. return err
  248. }
  249. } else {
  250. return os.ErrNotExist
  251. }
  252. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  253. tree, err := gitRepo.WriteTree()
  254. if err != nil {
  255. return err
  256. }
  257. message := "Delete page '" + wikiName + "'"
  258. commitTreeOpts := git.CommitTreeOpts{
  259. Message: message,
  260. Parents: []string{"HEAD"},
  261. }
  262. sign, signingKey, _ := repo.SignWikiCommit(doer)
  263. if sign {
  264. commitTreeOpts.KeyID = signingKey
  265. } else {
  266. commitTreeOpts.NoGPGSign = true
  267. }
  268. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  269. if err != nil {
  270. return err
  271. }
  272. if err := git.Push(basePath, git.PushOptions{
  273. Remote: "origin",
  274. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  275. Env: models.PushingEnvironment(doer, repo),
  276. }); err != nil {
  277. if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) {
  278. return err
  279. }
  280. return fmt.Errorf("Push: %v", err)
  281. }
  282. return nil
  283. }