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.

253 lines
6.6 KiB

  1. // Copyright 2017 The Gitea 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 cmd
  5. import (
  6. "bufio"
  7. "bytes"
  8. "fmt"
  9. "net/url"
  10. "os"
  11. "strconv"
  12. "strings"
  13. "code.gitea.io/git"
  14. "code.gitea.io/gitea/models"
  15. "code.gitea.io/gitea/modules/log"
  16. "code.gitea.io/gitea/modules/private"
  17. "code.gitea.io/gitea/modules/setting"
  18. "github.com/urfave/cli"
  19. )
  20. var (
  21. // CmdHook represents the available hooks sub-command.
  22. CmdHook = cli.Command{
  23. Name: "hook",
  24. Usage: "Delegate commands to corresponding Git hooks",
  25. Description: "This should only be called by Git",
  26. Flags: []cli.Flag{
  27. cli.StringFlag{
  28. Name: "config, c",
  29. Value: "custom/conf/app.ini",
  30. Usage: "Custom configuration file path",
  31. },
  32. },
  33. Subcommands: []cli.Command{
  34. subcmdHookPreReceive,
  35. subcmdHookUpdate,
  36. subcmdHookPostReceive,
  37. },
  38. }
  39. subcmdHookPreReceive = cli.Command{
  40. Name: "pre-receive",
  41. Usage: "Delegate pre-receive Git hook",
  42. Description: "This command should only be called by Git",
  43. Action: runHookPreReceive,
  44. }
  45. subcmdHookUpdate = cli.Command{
  46. Name: "update",
  47. Usage: "Delegate update Git hook",
  48. Description: "This command should only be called by Git",
  49. Action: runHookUpdate,
  50. }
  51. subcmdHookPostReceive = cli.Command{
  52. Name: "post-receive",
  53. Usage: "Delegate post-receive Git hook",
  54. Description: "This command should only be called by Git",
  55. Action: runHookPostReceive,
  56. }
  57. )
  58. func runHookPreReceive(c *cli.Context) error {
  59. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  60. return nil
  61. }
  62. if c.IsSet("config") {
  63. setting.CustomConf = c.String("config")
  64. } else if c.GlobalIsSet("config") {
  65. setting.CustomConf = c.GlobalString("config")
  66. }
  67. setup("hooks/pre-receive.log")
  68. // the environment setted on serv command
  69. repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
  70. isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
  71. username := os.Getenv(models.EnvRepoUsername)
  72. reponame := os.Getenv(models.EnvRepoName)
  73. userIDStr := os.Getenv(models.EnvPusherID)
  74. repoPath := models.RepoPath(username, reponame)
  75. buf := bytes.NewBuffer(nil)
  76. scanner := bufio.NewScanner(os.Stdin)
  77. for scanner.Scan() {
  78. buf.Write(scanner.Bytes())
  79. buf.WriteByte('\n')
  80. // TODO: support news feeds for wiki
  81. if isWiki {
  82. continue
  83. }
  84. fields := bytes.Fields(scanner.Bytes())
  85. if len(fields) != 3 {
  86. continue
  87. }
  88. oldCommitID := string(fields[0])
  89. newCommitID := string(fields[1])
  90. refFullName := string(fields[2])
  91. branchName := strings.TrimPrefix(refFullName, git.BranchPrefix)
  92. protectBranch, err := private.GetProtectedBranchBy(repoID, branchName)
  93. if err != nil {
  94. fail("Internal error", fmt.Sprintf("retrieve protected branches information failed: %v", err))
  95. }
  96. if protectBranch != nil && protectBranch.IsProtected() {
  97. // check and deletion
  98. if newCommitID == git.EmptySHA {
  99. fail(fmt.Sprintf("branch %s is protected from deletion", branchName), "")
  100. }
  101. // detect force push
  102. if git.EmptySHA != oldCommitID {
  103. output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+newCommitID).RunInDir(repoPath)
  104. if err != nil {
  105. fail("Internal error", "Fail to detect force push: %v", err)
  106. } else if len(output) > 0 {
  107. fail(fmt.Sprintf("branch %s is protected from force push", branchName), "")
  108. }
  109. }
  110. userID, _ := strconv.ParseInt(userIDStr, 10, 64)
  111. canPush, err := private.CanUserPush(protectBranch.ID, userID)
  112. if err != nil {
  113. fail("Internal error", "Fail to detect user can push: %v", err)
  114. } else if !canPush {
  115. fail(fmt.Sprintf("protected branch %s can not be pushed to", branchName), "")
  116. }
  117. }
  118. }
  119. return nil
  120. }
  121. func runHookUpdate(c *cli.Context) error {
  122. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  123. return nil
  124. }
  125. if c.IsSet("config") {
  126. setting.CustomConf = c.String("config")
  127. } else if c.GlobalIsSet("config") {
  128. setting.CustomConf = c.GlobalString("config")
  129. }
  130. setup("hooks/update.log")
  131. return nil
  132. }
  133. func runHookPostReceive(c *cli.Context) error {
  134. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  135. return nil
  136. }
  137. if c.IsSet("config") {
  138. setting.CustomConf = c.String("config")
  139. } else if c.GlobalIsSet("config") {
  140. setting.CustomConf = c.GlobalString("config")
  141. }
  142. setup("hooks/post-receive.log")
  143. // the environment setted on serv command
  144. repoID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchRepoID), 10, 64)
  145. repoUser := os.Getenv(models.EnvRepoUsername)
  146. isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
  147. repoName := os.Getenv(models.EnvRepoName)
  148. pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
  149. pusherName := os.Getenv(models.EnvPusherName)
  150. buf := bytes.NewBuffer(nil)
  151. scanner := bufio.NewScanner(os.Stdin)
  152. for scanner.Scan() {
  153. buf.Write(scanner.Bytes())
  154. buf.WriteByte('\n')
  155. // TODO: support news feeds for wiki
  156. if isWiki {
  157. continue
  158. }
  159. fields := bytes.Fields(scanner.Bytes())
  160. if len(fields) != 3 {
  161. continue
  162. }
  163. oldCommitID := string(fields[0])
  164. newCommitID := string(fields[1])
  165. refFullName := string(fields[2])
  166. if err := private.PushUpdate(models.PushUpdateOptions{
  167. RefFullName: refFullName,
  168. OldCommitID: oldCommitID,
  169. NewCommitID: newCommitID,
  170. PusherID: pusherID,
  171. PusherName: pusherName,
  172. RepoUserName: repoUser,
  173. RepoName: repoName,
  174. }); err != nil {
  175. log.GitLogger.Error(2, "Update: %v", err)
  176. }
  177. if newCommitID != git.EmptySHA && strings.HasPrefix(refFullName, git.BranchPrefix) {
  178. branch := strings.TrimPrefix(refFullName, git.BranchPrefix)
  179. repo, pullRequestAllowed, err := private.GetRepository(repoID)
  180. if err != nil {
  181. log.GitLogger.Error(2, "get repo: %v", err)
  182. break
  183. }
  184. if !pullRequestAllowed {
  185. break
  186. }
  187. baseRepo := repo
  188. if repo.IsFork {
  189. baseRepo = repo.BaseRepo
  190. }
  191. if !repo.IsFork && branch == baseRepo.DefaultBranch {
  192. break
  193. }
  194. pr, err := private.ActivePullRequest(baseRepo.ID, repo.ID, baseRepo.DefaultBranch, branch)
  195. if err != nil {
  196. log.GitLogger.Error(2, "get active pr: %v", err)
  197. break
  198. }
  199. fmt.Fprintln(os.Stderr, "")
  200. if pr == nil {
  201. if repo.IsFork {
  202. branch = fmt.Sprintf("%s:%s", repo.OwnerName, branch)
  203. }
  204. fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", branch)
  205. fmt.Fprintf(os.Stderr, " %s/compare/%s...%s\n", baseRepo.HTMLURL(), url.QueryEscape(baseRepo.DefaultBranch), url.QueryEscape(branch))
  206. } else {
  207. fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
  208. fmt.Fprintf(os.Stderr, " %s/pulls/%d\n", baseRepo.HTMLURL(), pr.Index)
  209. }
  210. fmt.Fprintln(os.Stderr, "")
  211. }
  212. }
  213. return nil
  214. }