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.

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