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.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  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. defer gitRepo.Close()
  120. if hasMasterBranch {
  121. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  122. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  123. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  124. }
  125. }
  126. newWikiPath := WikiNameToFilename(newWikiName)
  127. if isNew {
  128. filesInIndex, err := gitRepo.LsFiles(newWikiPath)
  129. if err != nil {
  130. log.Error("%v", err)
  131. return err
  132. }
  133. for _, file := range filesInIndex {
  134. if file == newWikiPath {
  135. return ErrWikiAlreadyExist{newWikiPath}
  136. }
  137. }
  138. } else {
  139. oldWikiPath := WikiNameToFilename(oldWikiName)
  140. filesInIndex, err := gitRepo.LsFiles(oldWikiPath)
  141. if err != nil {
  142. log.Error("%v", err)
  143. return err
  144. }
  145. found := false
  146. for _, file := range filesInIndex {
  147. if file == oldWikiPath {
  148. found = true
  149. break
  150. }
  151. }
  152. if found {
  153. err := gitRepo.RemoveFilesFromIndex(oldWikiPath)
  154. if err != nil {
  155. log.Error("%v", err)
  156. return err
  157. }
  158. }
  159. }
  160. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  161. objectHash, err := gitRepo.HashObject(strings.NewReader(content))
  162. if err != nil {
  163. log.Error("%v", err)
  164. return err
  165. }
  166. if err := gitRepo.AddObjectToIndex("100644", objectHash, newWikiPath); err != nil {
  167. log.Error("%v", err)
  168. return err
  169. }
  170. tree, err := gitRepo.WriteTree()
  171. if err != nil {
  172. log.Error("%v", err)
  173. return err
  174. }
  175. commitTreeOpts := git.CommitTreeOpts{
  176. Message: message,
  177. }
  178. if hasMasterBranch {
  179. commitTreeOpts.Parents = []string{"HEAD"}
  180. }
  181. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, commitTreeOpts)
  182. if err != nil {
  183. log.Error("%v", err)
  184. return err
  185. }
  186. if err := git.Push(basePath, git.PushOptions{
  187. Remote: "origin",
  188. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  189. Env: FullPushingEnvironment(
  190. doer,
  191. doer,
  192. repo,
  193. repo.Name+".wiki",
  194. 0,
  195. ),
  196. }); err != nil {
  197. log.Error("%v", err)
  198. return fmt.Errorf("Push: %v", err)
  199. }
  200. return nil
  201. }
  202. // AddWikiPage adds a new wiki page with a given wikiPath.
  203. func (repo *Repository) AddWikiPage(doer *User, wikiName, content, message string) error {
  204. return repo.updateWikiPage(doer, "", wikiName, content, message, true)
  205. }
  206. // EditWikiPage updates a wiki page identified by its wikiPath,
  207. // optionally also changing wikiPath.
  208. func (repo *Repository) EditWikiPage(doer *User, oldWikiName, newWikiName, content, message string) error {
  209. return repo.updateWikiPage(doer, oldWikiName, newWikiName, content, message, false)
  210. }
  211. // DeleteWikiPage deletes a wiki page identified by its path.
  212. func (repo *Repository) DeleteWikiPage(doer *User, wikiName string) (err error) {
  213. wikiWorkingPool.CheckIn(com.ToStr(repo.ID))
  214. defer wikiWorkingPool.CheckOut(com.ToStr(repo.ID))
  215. if err = repo.InitWiki(); err != nil {
  216. return fmt.Errorf("InitWiki: %v", err)
  217. }
  218. basePath, err := CreateTemporaryPath("update-wiki")
  219. if err != nil {
  220. return err
  221. }
  222. defer func() {
  223. if err := RemoveTemporaryPath(basePath); err != nil {
  224. log.Error("Merge: RemoveTemporaryPath: %s", err)
  225. }
  226. }()
  227. if err := git.Clone(repo.WikiPath(), basePath, git.CloneRepoOptions{
  228. Bare: true,
  229. Shared: true,
  230. Branch: "master",
  231. }); err != nil {
  232. log.Error("Failed to clone repository: %s (%v)", repo.FullName(), err)
  233. return fmt.Errorf("Failed to clone repository: %s (%v)", repo.FullName(), err)
  234. }
  235. gitRepo, err := git.OpenRepository(basePath)
  236. if err != nil {
  237. log.Error("Unable to open temporary repository: %s (%v)", basePath, err)
  238. return fmt.Errorf("Failed to open new temporary repository in: %s %v", basePath, err)
  239. }
  240. defer gitRepo.Close()
  241. if err := gitRepo.ReadTreeToIndex("HEAD"); err != nil {
  242. log.Error("Unable to read HEAD tree to index in: %s %v", basePath, err)
  243. return fmt.Errorf("Unable to read HEAD tree to index in: %s %v", basePath, err)
  244. }
  245. wikiPath := WikiNameToFilename(wikiName)
  246. filesInIndex, err := gitRepo.LsFiles(wikiPath)
  247. found := false
  248. for _, file := range filesInIndex {
  249. if file == wikiPath {
  250. found = true
  251. break
  252. }
  253. }
  254. if found {
  255. err := gitRepo.RemoveFilesFromIndex(wikiPath)
  256. if err != nil {
  257. return err
  258. }
  259. } else {
  260. return os.ErrNotExist
  261. }
  262. // FIXME: The wiki doesn't have lfs support at present - if this changes need to check attributes here
  263. tree, err := gitRepo.WriteTree()
  264. if err != nil {
  265. return err
  266. }
  267. message := "Delete page '" + wikiName + "'"
  268. commitHash, err := gitRepo.CommitTree(doer.NewGitSig(), tree, git.CommitTreeOpts{
  269. Message: message,
  270. Parents: []string{"HEAD"},
  271. })
  272. if err != nil {
  273. return err
  274. }
  275. if err := git.Push(basePath, git.PushOptions{
  276. Remote: "origin",
  277. Branch: fmt.Sprintf("%s:%s%s", commitHash.String(), git.BranchPrefix, "master"),
  278. Env: PushingEnvironment(doer, repo),
  279. }); err != nil {
  280. return fmt.Errorf("Push: %v", err)
  281. }
  282. return nil
  283. }