hook.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. // Copyright 2014 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 cmd
  5. import (
  6. "bufio"
  7. "bytes"
  8. "crypto/tls"
  9. "fmt"
  10. "os"
  11. "os/exec"
  12. "path"
  13. "path/filepath"
  14. "strings"
  15. "github.com/Unknwon/com"
  16. "github.com/urfave/cli"
  17. log "gopkg.in/clog.v1"
  18. "github.com/gogits/git-module"
  19. "github.com/gogits/gogs/models"
  20. "github.com/gogits/gogs/pkg/httplib"
  21. "github.com/gogits/gogs/pkg/mailer"
  22. "github.com/gogits/gogs/pkg/setting"
  23. "github.com/gogits/gogs/pkg/template"
  24. http "github.com/gogits/gogs/routes/repo"
  25. )
  26. var (
  27. Hook = cli.Command{
  28. Name: "hook",
  29. Usage: "Delegate commands to corresponding Git hooks",
  30. Description: "All sub-commands should only be called by Git",
  31. Flags: []cli.Flag{
  32. stringFlag("config, c", "custom/conf/app.ini", "Custom configuration file path"),
  33. },
  34. Subcommands: []cli.Command{
  35. subcmdHookPreReceive,
  36. subcmdHookUpadte,
  37. subcmdHookPostReceive,
  38. },
  39. }
  40. subcmdHookPreReceive = cli.Command{
  41. Name: "pre-receive",
  42. Usage: "Delegate pre-receive Git hook",
  43. Description: "This command should only be called by Git",
  44. Action: runHookPreReceive,
  45. }
  46. subcmdHookUpadte = cli.Command{
  47. Name: "update",
  48. Usage: "Delegate update Git hook",
  49. Description: "This command should only be called by Git",
  50. Action: runHookUpdate,
  51. }
  52. subcmdHookPostReceive = cli.Command{
  53. Name: "post-receive",
  54. Usage: "Delegate post-receive Git hook",
  55. Description: "This command should only be called by Git",
  56. Action: runHookPostReceive,
  57. }
  58. )
  59. func runHookPreReceive(c *cli.Context) error {
  60. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  61. return nil
  62. }
  63. setup(c, "hooks/pre-receive.log", true)
  64. isWiki := strings.Contains(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
  65. buf := bytes.NewBuffer(nil)
  66. scanner := bufio.NewScanner(os.Stdin)
  67. for scanner.Scan() {
  68. buf.Write(scanner.Bytes())
  69. buf.WriteByte('\n')
  70. if isWiki {
  71. continue
  72. }
  73. fields := bytes.Fields(scanner.Bytes())
  74. if len(fields) != 3 {
  75. continue
  76. }
  77. oldCommitID := string(fields[0])
  78. newCommitID := string(fields[1])
  79. branchName := strings.TrimPrefix(string(fields[2]), git.BRANCH_PREFIX)
  80. // Branch protection
  81. repoID := com.StrTo(os.Getenv(http.ENV_REPO_ID)).MustInt64()
  82. protectBranch, err := models.GetProtectBranchOfRepoByName(repoID, branchName)
  83. if err != nil {
  84. if models.IsErrBranchNotExist(err) {
  85. continue
  86. }
  87. fail("Internal error", "GetProtectBranchOfRepoByName [repo_id: %d, branch: %s]: %v", repoID, branchName, err)
  88. }
  89. if !protectBranch.Protected {
  90. continue
  91. }
  92. // Whitelist users can bypass require pull request check
  93. bypassRequirePullRequest := false
  94. // Check if user is in whitelist when enabled
  95. userID := com.StrTo(os.Getenv(http.ENV_AUTH_USER_ID)).MustInt64()
  96. if protectBranch.EnableWhitelist {
  97. if !models.IsUserInProtectBranchWhitelist(repoID, userID, branchName) {
  98. fail(fmt.Sprintf("Branch '%s' is protected and you are not in the push whitelist", branchName), "")
  99. }
  100. bypassRequirePullRequest = true
  101. }
  102. // Check if branch allows direct push
  103. if !bypassRequirePullRequest && protectBranch.RequirePullRequest {
  104. fail(fmt.Sprintf("Branch '%s' is protected and commits must be merged through pull request", branchName), "")
  105. }
  106. // check and deletion
  107. if newCommitID == git.EMPTY_SHA {
  108. fail(fmt.Sprintf("Branch '%s' is protected from deletion", branchName), "")
  109. }
  110. // Check force push
  111. output, err := git.NewCommand("rev-list", oldCommitID, "^"+newCommitID).
  112. RunInDir(models.RepoPath(os.Getenv(http.ENV_REPO_OWNER_NAME), os.Getenv(http.ENV_REPO_NAME)))
  113. if err != nil {
  114. fail("Internal error", "Fail to detect force push: %v", err)
  115. } else if len(output) > 0 {
  116. fail(fmt.Sprintf("Branch '%s' is protected from force push", branchName), "")
  117. }
  118. }
  119. customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive")
  120. if !com.IsFile(customHooksPath) {
  121. return nil
  122. }
  123. hookCmd := exec.Command(customHooksPath)
  124. hookCmd.Dir = models.RepoPath(os.Getenv(http.ENV_REPO_OWNER_NAME), os.Getenv(http.ENV_REPO_NAME))
  125. hookCmd.Stdout = os.Stdout
  126. hookCmd.Stdin = buf
  127. hookCmd.Stderr = os.Stderr
  128. if err := hookCmd.Run(); err != nil {
  129. fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
  130. }
  131. return nil
  132. }
  133. func runHookUpdate(c *cli.Context) error {
  134. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  135. return nil
  136. }
  137. setup(c, "hooks/update.log", false)
  138. args := c.Args()
  139. if len(args) != 3 {
  140. fail("Arguments received are not equal to three", "Arguments received are not equal to three")
  141. } else if len(args[0]) == 0 {
  142. fail("First argument 'refName' is empty", "First argument 'refName' is empty")
  143. }
  144. customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "update")
  145. if !com.IsFile(customHooksPath) {
  146. return nil
  147. }
  148. hookCmd := exec.Command(customHooksPath, args...)
  149. hookCmd.Dir = models.RepoPath(os.Getenv(http.ENV_REPO_OWNER_NAME), os.Getenv(http.ENV_REPO_NAME))
  150. hookCmd.Stdout = os.Stdout
  151. hookCmd.Stdin = os.Stdin
  152. hookCmd.Stderr = os.Stderr
  153. if err := hookCmd.Run(); err != nil {
  154. fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
  155. }
  156. return nil
  157. }
  158. func runHookPostReceive(c *cli.Context) error {
  159. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  160. return nil
  161. }
  162. setup(c, "hooks/post-receive.log", true)
  163. // Post-receive hook does more than just gather Git information,
  164. // so we need to setup additional services for email notifications.
  165. setting.NewPostReceiveHookServices()
  166. mailer.NewContext()
  167. mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
  168. path.Join(setting.CustomPath, "templates/mail"), template.NewFuncMap())
  169. isWiki := strings.Contains(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
  170. buf := bytes.NewBuffer(nil)
  171. scanner := bufio.NewScanner(os.Stdin)
  172. for scanner.Scan() {
  173. buf.Write(scanner.Bytes())
  174. buf.WriteByte('\n')
  175. // TODO: support news feeds for wiki
  176. if isWiki {
  177. continue
  178. }
  179. fields := bytes.Fields(scanner.Bytes())
  180. if len(fields) != 3 {
  181. continue
  182. }
  183. options := models.PushUpdateOptions{
  184. OldCommitID: string(fields[0]),
  185. NewCommitID: string(fields[1]),
  186. RefFullName: string(fields[2]),
  187. PusherID: com.StrTo(os.Getenv(http.ENV_AUTH_USER_ID)).MustInt64(),
  188. PusherName: os.Getenv(http.ENV_AUTH_USER_NAME),
  189. RepoUserName: os.Getenv(http.ENV_REPO_OWNER_NAME),
  190. RepoName: os.Getenv(http.ENV_REPO_NAME),
  191. }
  192. if err := models.PushUpdate(options); err != nil {
  193. log.Error(2, "PushUpdate: %v", err)
  194. }
  195. // Ask for running deliver hook and test pull request tasks.
  196. reqURL := setting.LocalURL + options.RepoUserName + "/" + options.RepoName + "/tasks/trigger?branch=" +
  197. strings.TrimPrefix(options.RefFullName, git.BRANCH_PREFIX) +
  198. "&secret=" + os.Getenv(http.ENV_REPO_OWNER_SALT_MD5) +
  199. "&pusher=" + os.Getenv(http.ENV_AUTH_USER_ID)
  200. log.Trace("Trigger task: %s", reqURL)
  201. resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
  202. InsecureSkipVerify: true,
  203. }).Response()
  204. if err == nil {
  205. resp.Body.Close()
  206. if resp.StatusCode/100 != 2 {
  207. log.Error(2, "Fail to trigger task: not 2xx response code")
  208. }
  209. } else {
  210. log.Error(2, "Fail to trigger task: %v", err)
  211. }
  212. }
  213. customHooksPath := filepath.Join(os.Getenv(http.ENV_REPO_CUSTOM_HOOKS_PATH), "post-receive")
  214. if !com.IsFile(customHooksPath) {
  215. return nil
  216. }
  217. hookCmd := exec.Command(customHooksPath)
  218. hookCmd.Dir = models.RepoPath(os.Getenv(http.ENV_REPO_OWNER_NAME), os.Getenv(http.ENV_REPO_NAME))
  219. hookCmd.Stdout = os.Stdout
  220. hookCmd.Stdin = buf
  221. hookCmd.Stderr = os.Stderr
  222. if err := hookCmd.Run(); err != nil {
  223. fail("Internal error", "Fail to execute custom post-receive hook: %v", err)
  224. }
  225. return nil
  226. }