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