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.

323 lines
8.5 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. return fmt.Errorf("Push: %v", err)
  186. }
  187. return nil
  188. }
  189. // AddWikiPage adds a new wiki page with a given wikiPath.
  190. func AddWikiPage(doer *models.User, repo *models.Repository, wikiName, content, message string) error {
  191. return updateWikiPage(doer, repo, "", wikiName, content, message, true)
  192. }
  193. // EditWikiPage updates a wiki page identified by its wikiPath,
  194. // optionally also changing wikiPath.
  195. func EditWikiPage(doer *models.User, repo *models.Repository, oldWikiName, newWikiName, content, message string) error {
  196. return updateWikiPage(doer, repo, oldWikiName, newWikiName, content, message, false)
  197. }
  198. // DeleteWikiPage deletes a wiki page identified by its path.
  199. func DeleteWikiPage(doer *models.User, repo *models.Repository, wikiName string) (err error) {
  200. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  201. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  202. if err = InitWiki(repo); err != nil {
  203. return fmt.Errorf("InitWiki: %v", err)
  204. }
  205. basePath, err := models.CreateTemporaryPath("update-wiki")
  206. if err != nil {
  207. return err
  208. }
  209. defer func() {
  210. if err := models.RemoveTemporaryPath(basePath); err != nil {
  211. log.Error("Merge: RemoveTemporaryPath: %s", err)
  212. }
  213. }()
  214. if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
  215. Bare: true,
  216. Shared: true,
  217. Branch: "master",
  218. }); err != nil {
  219. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  220. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  221. }
  222. gitRepo, err := git.OpenRepository(basePath)
  223. if err != nil {
  224. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  225. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  226. }
  227. defer gitRepo.Close()
  228. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  229. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  230. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  231. }
  232. wikiPath := NameToFilename(wikiName)
  233. filesInIndex, err := gitRepo.LsFiles(wikiPath)
  234. found := false
  235. for _, file := range filesInIndex {
  236. if file == wikiPath {
  237. found = true
  238. break
  239. }
  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 := "Delete page '" + wikiName + "'"
  255. commitTreeOpts := git.CommitTreeOpts{
  256. Message: message,
  257. Parents: []string{"HEAD"},
  258. }
  259. sign, signingKey, _ := repo.SignWikiCommit(doer)
  260. if sign {
  261. commitTreeOpts.KeyID = signingKey
  262. } else {
  263. commitTreeOpts.NoGPGSign = true
  264. }
  265. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  266. if err != nil {
  267. return err
  268. }
  269. if err := git.Push(basePath, git.PushOptions{
  270. Remote: "origin",
  271. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  272. Env: models.PushingEnvironment(doer, repo),
  273. }); err != nil {
  274. return fmt.Errorf("Push: %v", err)
  275. }
  276. return nil
  277. }