hook.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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/gogs/git-module"
  19. "github.com/gogs/gogs/models"
  20. "github.com/gogs/gogs/models/errors"
  21. "github.com/gogs/gogs/pkg/httplib"
  22. "github.com/gogs/gogs/pkg/mailer"
  23. "github.com/gogs/gogs/pkg/setting"
  24. "github.com/gogs/gogs/pkg/template"
  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(models.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(models.ENV_REPO_ID)).MustInt64()
  82. protectBranch, err := models.GetProtectBranchOfRepoByName(repoID, branchName)
  83. if err != nil {
  84. if errors.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(models.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", "--max-count=1", oldCommitID, "^"+newCommitID).
  112. RunInDir(models.RepoPath(os.Getenv(models.ENV_REPO_OWNER_NAME), os.Getenv(models.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(models.ENV_REPO_CUSTOM_HOOKS_PATH), "pre-receive")
  120. if !com.IsFile(customHooksPath) {
  121. return nil
  122. }
  123. var hookCmd *exec.Cmd
  124. if setting.IsWindows {
  125. hookCmd = exec.Command("bash.exe", "custom_hooks/pre-receive")
  126. } else {
  127. hookCmd = exec.Command(customHooksPath)
  128. }
  129. hookCmd.Dir = models.RepoPath(os.Getenv(models.ENV_REPO_OWNER_NAME), os.Getenv(models.ENV_REPO_NAME))
  130. hookCmd.Stdout = os.Stdout
  131. hookCmd.Stdin = buf
  132. hookCmd.Stderr = os.Stderr
  133. if err := hookCmd.Run(); err != nil {
  134. fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
  135. }
  136. return nil
  137. }
  138. func runHookUpdate(c *cli.Context) error {
  139. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  140. return nil
  141. }
  142. setup(c, "hooks/update.log", false)
  143. args := c.Args()
  144. if len(args) != 3 {
  145. fail("Arguments received are not equal to three", "Arguments received are not equal to three")
  146. } else if len(args[0]) == 0 {
  147. fail("First argument 'refName' is empty", "First argument 'refName' is empty")
  148. }
  149. customHooksPath := filepath.Join(os.Getenv(models.ENV_REPO_CUSTOM_HOOKS_PATH), "update")
  150. if !com.IsFile(customHooksPath) {
  151. return nil
  152. }
  153. var hookCmd *exec.Cmd
  154. if setting.IsWindows {
  155. hookCmd = exec.Command("bash.exe", append([]string{"custom_hooks/update"}, args...)...)
  156. } else {
  157. hookCmd = exec.Command(customHooksPath, args...)
  158. }
  159. hookCmd.Dir = models.RepoPath(os.Getenv(models.ENV_REPO_OWNER_NAME), os.Getenv(models.ENV_REPO_NAME))
  160. hookCmd.Stdout = os.Stdout
  161. hookCmd.Stdin = os.Stdin
  162. hookCmd.Stderr = os.Stderr
  163. if err := hookCmd.Run(); err != nil {
  164. fail("Internal error", "Fail to execute custom pre-receive hook: %v", err)
  165. }
  166. return nil
  167. }
  168. func runHookPostReceive(c *cli.Context) error {
  169. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  170. return nil
  171. }
  172. setup(c, "hooks/post-receive.log", true)
  173. // Post-receive hook does more than just gather Git information,
  174. // so we need to setup additional services for email notifications.
  175. setting.NewPostReceiveHookServices()
  176. mailer.NewContext()
  177. mailer.InitMailRender(path.Join(setting.StaticRootPath, "templates/mail"),
  178. path.Join(setting.CustomPath, "templates/mail"), template.NewFuncMap())
  179. isWiki := strings.Contains(os.Getenv(models.ENV_REPO_CUSTOM_HOOKS_PATH), ".wiki.git/")
  180. buf := bytes.NewBuffer(nil)
  181. scanner := bufio.NewScanner(os.Stdin)
  182. for scanner.Scan() {
  183. buf.Write(scanner.Bytes())
  184. buf.WriteByte('\n')
  185. // TODO: support news feeds for wiki
  186. if isWiki {
  187. continue
  188. }
  189. fields := bytes.Fields(scanner.Bytes())
  190. if len(fields) != 3 {
  191. continue
  192. }
  193. options := models.PushUpdateOptions{
  194. OldCommitID: string(fields[0]),
  195. NewCommitID: string(fields[1]),
  196. RefFullName: string(fields[2]),
  197. PusherID: com.StrTo(os.Getenv(models.ENV_AUTH_USER_ID)).MustInt64(),
  198. PusherName: os.Getenv(models.ENV_AUTH_USER_NAME),
  199. RepoUserName: os.Getenv(models.ENV_REPO_OWNER_NAME),
  200. RepoName: os.Getenv(models.ENV_REPO_NAME),
  201. }
  202. if err := models.PushUpdate(options); err != nil {
  203. log.Error(2, "PushUpdate: %v", err)
  204. }
  205. // Ask for running deliver hook and test pull request tasks
  206. reqURL := setting.LocalURL + options.RepoUserName + "/" + options.RepoName + "/tasks/trigger?branch=" +
  207. template.EscapePound(strings.TrimPrefix(options.RefFullName, git.BRANCH_PREFIX)) +
  208. "&secret=" + os.Getenv(models.ENV_REPO_OWNER_SALT_MD5) +
  209. "&pusher=" + os.Getenv(models.ENV_AUTH_USER_ID)
  210. log.Trace("Trigger task: %s", reqURL)
  211. resp, err := httplib.Head(reqURL).SetTLSClientConfig(&tls.Config{
  212. InsecureSkipVerify: true,
  213. }).Response()
  214. if err == nil {
  215. resp.Body.Close()
  216. if resp.StatusCode/100 != 2 {
  217. log.Error(2, "Fail to trigger task: not 2xx response code")
  218. }
  219. } else {
  220. log.Error(2, "Fail to trigger task: %v", err)
  221. }
  222. }
  223. customHooksPath := filepath.Join(os.Getenv(models.ENV_REPO_CUSTOM_HOOKS_PATH), "post-receive")
  224. if !com.IsFile(customHooksPath) {
  225. return nil
  226. }
  227. var hookCmd *exec.Cmd
  228. if setting.IsWindows {
  229. hookCmd = exec.Command("bash.exe", "custom_hooks/post-receive")
  230. } else {
  231. hookCmd = exec.Command(customHooksPath)
  232. }
  233. hookCmd.Dir = models.RepoPath(os.Getenv(models.ENV_REPO_OWNER_NAME), os.Getenv(models.ENV_REPO_NAME))
  234. hookCmd.Stdout = os.Stdout
  235. hookCmd.Stdin = buf
  236. hookCmd.Stderr = os.Stderr
  237. if err := hookCmd.Run(); err != nil {
  238. fail("Internal error", "Fail to execute custom post-receive hook: %v", err)
  239. }
  240. return nil
  241. }