hook.go 8.2 KB

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