hook.go 7.8 KB

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