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

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