You can not select more than 25 topics Topics must start with a chinese character,a letter or number, can include dashes ('-') and can be up to 35 characters long.

hook.go 12 kB

3 years ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. // Copyright 2017 The Gitea 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. "fmt"
  9. "io"
  10. "net/http"
  11. "os"
  12. "strconv"
  13. "strings"
  14. "time"
  15. "code.gitea.io/gitea/models"
  16. "code.gitea.io/gitea/modules/git"
  17. "code.gitea.io/gitea/modules/private"
  18. "code.gitea.io/gitea/modules/setting"
  19. "code.gitea.io/gitea/modules/util"
  20. "github.com/urfave/cli"
  21. )
  22. const (
  23. hookBatchSize = 30
  24. )
  25. var (
  26. // CmdHook represents the available hooks sub-command.
  27. CmdHook = cli.Command{
  28. Name: "hook",
  29. Usage: "Delegate commands to corresponding Git hooks",
  30. Description: "This should only be called by Git",
  31. Subcommands: []cli.Command{
  32. subcmdHookPreReceive,
  33. subcmdHookUpdate,
  34. subcmdHookPostReceive,
  35. },
  36. }
  37. subcmdHookPreReceive = cli.Command{
  38. Name: "pre-receive",
  39. Usage: "Delegate pre-receive Git hook",
  40. Description: "This command should only be called by Git",
  41. Action: runHookPreReceive,
  42. }
  43. subcmdHookUpdate = cli.Command{
  44. Name: "update",
  45. Usage: "Delegate update Git hook",
  46. Description: "This command should only be called by Git",
  47. Action: runHookUpdate,
  48. }
  49. subcmdHookPostReceive = cli.Command{
  50. Name: "post-receive",
  51. Usage: "Delegate post-receive Git hook",
  52. Description: "This command should only be called by Git",
  53. Action: runHookPostReceive,
  54. }
  55. )
  56. type delayWriter struct {
  57. internal io.Writer
  58. buf *bytes.Buffer
  59. timer *time.Timer
  60. }
  61. func newDelayWriter(internal io.Writer, delay time.Duration) *delayWriter {
  62. timer := time.NewTimer(delay)
  63. return &delayWriter{
  64. internal: internal,
  65. buf: &bytes.Buffer{},
  66. timer: timer,
  67. }
  68. }
  69. func (d *delayWriter) Write(p []byte) (n int, err error) {
  70. if d.buf != nil {
  71. select {
  72. case <-d.timer.C:
  73. _, err := d.internal.Write(d.buf.Bytes())
  74. if err != nil {
  75. return 0, err
  76. }
  77. d.buf = nil
  78. return d.internal.Write(p)
  79. default:
  80. return d.buf.Write(p)
  81. }
  82. }
  83. return d.internal.Write(p)
  84. }
  85. func (d *delayWriter) WriteString(s string) (n int, err error) {
  86. if d.buf != nil {
  87. select {
  88. case <-d.timer.C:
  89. _, err := d.internal.Write(d.buf.Bytes())
  90. if err != nil {
  91. return 0, err
  92. }
  93. d.buf = nil
  94. return d.internal.Write([]byte(s))
  95. default:
  96. return d.buf.WriteString(s)
  97. }
  98. }
  99. return d.internal.Write([]byte(s))
  100. }
  101. func (d *delayWriter) Close() error {
  102. if d == nil {
  103. return nil
  104. }
  105. stopped := util.StopTimer(d.timer)
  106. if stopped || d.buf == nil {
  107. return nil
  108. }
  109. _, err := d.internal.Write(d.buf.Bytes())
  110. d.buf = nil
  111. return err
  112. }
  113. type nilWriter struct{}
  114. func (n *nilWriter) Write(p []byte) (int, error) {
  115. return len(p), nil
  116. }
  117. func (n *nilWriter) WriteString(s string) (int, error) {
  118. return len(s), nil
  119. }
  120. func runHookPreReceive(c *cli.Context) error {
  121. if os.Getenv(models.EnvIsInternal) == "true" {
  122. return nil
  123. }
  124. setup("hooks/pre-receive.log", false)
  125. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  126. if setting.OnlyAllowPushIfGiteaEnvironmentSet {
  127. fail(`Rejecting changes as Gitea environment not set.
  128. If you are pushing over SSH you must push with a key managed by
  129. Gitea or set your environment appropriately.`, "")
  130. } else {
  131. return nil
  132. }
  133. }
  134. // the environment setted on serv command
  135. isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
  136. username := os.Getenv(models.EnvRepoUsername)
  137. reponame := os.Getenv(models.EnvRepoName)
  138. userID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
  139. prID, _ := strconv.ParseInt(os.Getenv(models.ProtectedBranchPRID), 10, 64)
  140. isDeployKey, _ := strconv.ParseBool(os.Getenv(models.EnvIsDeployKey))
  141. //set environment for pre-receive hook script
  142. os.Setenv(models.EnvRepoMaxFileSize, fmt.Sprint(setting.Repository.Upload.FileMaxSize))
  143. os.Setenv(models.EnvRepoMaxSize, fmt.Sprint(setting.Repository.RepoMaxSize))
  144. os.Setenv(models.EnvPushSizeCheckFlag, fmt.Sprint(setting.Repository.Upload.ShellFlag))
  145. env, _ := private.GetHookConfig(username, reponame)
  146. if env != nil && len(env) > 0 {
  147. repoSize := env[models.EnvRepoSize]
  148. if repoSize != "" {
  149. os.Setenv(models.EnvRepoSize, repoSize)
  150. }
  151. }
  152. hookOptions := private.HookOptions{
  153. UserID: userID,
  154. GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
  155. GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
  156. GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
  157. ProtectedBranchID: prID,
  158. IsDeployKey: isDeployKey,
  159. }
  160. scanner := bufio.NewScanner(os.Stdin)
  161. oldCommitIDs := make([]string, hookBatchSize)
  162. newCommitIDs := make([]string, hookBatchSize)
  163. refFullNames := make([]string, hookBatchSize)
  164. count := 0
  165. total := 0
  166. lastline := 0
  167. var out io.Writer
  168. out = &nilWriter{}
  169. if setting.Git.VerbosePush {
  170. if setting.Git.VerbosePushDelay > 0 {
  171. dWriter := newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
  172. defer dWriter.Close()
  173. out = dWriter
  174. } else {
  175. out = os.Stdout
  176. }
  177. }
  178. for scanner.Scan() {
  179. // TODO: support news feeds for wiki
  180. if isWiki {
  181. continue
  182. }
  183. fields := bytes.Fields(scanner.Bytes())
  184. if len(fields) != 3 {
  185. continue
  186. }
  187. oldCommitID := string(fields[0])
  188. newCommitID := string(fields[1])
  189. refFullName := string(fields[2])
  190. total++
  191. lastline++
  192. // If the ref is a branch, check if it's protected
  193. if strings.HasPrefix(refFullName, git.BranchPrefix) {
  194. oldCommitIDs[count] = oldCommitID
  195. newCommitIDs[count] = newCommitID
  196. refFullNames[count] = refFullName
  197. count++
  198. fmt.Fprintf(out, "*")
  199. if count >= hookBatchSize {
  200. fmt.Fprintf(out, " Checking %d branches\n", count)
  201. hookOptions.OldCommitIDs = oldCommitIDs
  202. hookOptions.NewCommitIDs = newCommitIDs
  203. hookOptions.RefFullNames = refFullNames
  204. statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
  205. switch statusCode {
  206. case http.StatusOK:
  207. // no-op
  208. case http.StatusInternalServerError:
  209. fail("Internal Server Error", msg)
  210. default:
  211. fail(msg, "")
  212. }
  213. count = 0
  214. lastline = 0
  215. }
  216. } else {
  217. fmt.Fprintf(out, ".")
  218. }
  219. if lastline >= hookBatchSize {
  220. fmt.Fprintf(out, "\n")
  221. lastline = 0
  222. }
  223. }
  224. if count > 0 {
  225. hookOptions.OldCommitIDs = oldCommitIDs[:count]
  226. hookOptions.NewCommitIDs = newCommitIDs[:count]
  227. hookOptions.RefFullNames = refFullNames[:count]
  228. fmt.Fprintf(out, " Checking %d branches\n", count)
  229. statusCode, msg := private.HookPreReceive(username, reponame, hookOptions)
  230. switch statusCode {
  231. case http.StatusInternalServerError:
  232. fail("Internal Server Error", msg)
  233. case http.StatusForbidden:
  234. fail(msg, "")
  235. }
  236. } else if lastline > 0 {
  237. fmt.Fprintf(out, "\n")
  238. lastline = 0
  239. }
  240. fmt.Fprintf(out, "Checked %d references in total\n", total)
  241. return nil
  242. }
  243. func runHookUpdate(c *cli.Context) error {
  244. // Update is empty and is kept only for backwards compatibility
  245. return nil
  246. }
  247. func runHookPostReceive(c *cli.Context) error {
  248. if os.Getenv(models.EnvIsInternal) == "true" {
  249. return nil
  250. }
  251. setup("hooks/post-receive.log", false)
  252. if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
  253. if setting.OnlyAllowPushIfGiteaEnvironmentSet {
  254. fail(`Rejecting changes as Gitea environment not set.
  255. If you are pushing over SSH you must push with a key managed by
  256. Gitea or set your environment appropriately.`, "")
  257. } else {
  258. return nil
  259. }
  260. }
  261. var out io.Writer
  262. var dWriter *delayWriter
  263. out = &nilWriter{}
  264. if setting.Git.VerbosePush {
  265. if setting.Git.VerbosePushDelay > 0 {
  266. dWriter = newDelayWriter(os.Stdout, setting.Git.VerbosePushDelay)
  267. defer dWriter.Close()
  268. out = dWriter
  269. } else {
  270. out = os.Stdout
  271. }
  272. }
  273. // the environment setted on serv command
  274. repoUser := os.Getenv(models.EnvRepoUsername)
  275. isWiki := (os.Getenv(models.EnvRepoIsWiki) == "true")
  276. repoName := os.Getenv(models.EnvRepoName)
  277. pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
  278. pusherName := os.Getenv(models.EnvPusherName)
  279. hookOptions := private.HookOptions{
  280. UserName: pusherName,
  281. UserID: pusherID,
  282. GitAlternativeObjectDirectories: os.Getenv(private.GitAlternativeObjectDirectories),
  283. GitObjectDirectory: os.Getenv(private.GitObjectDirectory),
  284. GitQuarantinePath: os.Getenv(private.GitQuarantinePath),
  285. }
  286. oldCommitIDs := make([]string, hookBatchSize)
  287. newCommitIDs := make([]string, hookBatchSize)
  288. refFullNames := make([]string, hookBatchSize)
  289. count := 0
  290. total := 0
  291. wasEmpty := false
  292. masterPushed := false
  293. results := make([]private.HookPostReceiveBranchResult, 0)
  294. scanner := bufio.NewScanner(os.Stdin)
  295. for scanner.Scan() {
  296. // TODO: support news feeds for wiki
  297. if isWiki {
  298. continue
  299. }
  300. fields := bytes.Fields(scanner.Bytes())
  301. if len(fields) != 3 {
  302. continue
  303. }
  304. fmt.Fprintf(out, ".")
  305. oldCommitIDs[count] = string(fields[0])
  306. newCommitIDs[count] = string(fields[1])
  307. refFullNames[count] = string(fields[2])
  308. if refFullNames[count] == git.BranchPrefix+"master" && newCommitIDs[count] != git.EmptySHA && count == total {
  309. masterPushed = true
  310. }
  311. count++
  312. total++
  313. if count >= hookBatchSize {
  314. fmt.Fprintf(out, " Processing %d references\n", count)
  315. hookOptions.OldCommitIDs = oldCommitIDs
  316. hookOptions.NewCommitIDs = newCommitIDs
  317. hookOptions.RefFullNames = refFullNames
  318. resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
  319. if resp == nil {
  320. _ = dWriter.Close()
  321. hookPrintResults(results)
  322. fail("Internal Server Error", err)
  323. }
  324. wasEmpty = wasEmpty || resp.RepoWasEmpty
  325. results = append(results, resp.Results...)
  326. count = 0
  327. }
  328. }
  329. if count == 0 {
  330. if wasEmpty && masterPushed {
  331. // We need to tell the repo to reset the default branch to master
  332. err := private.SetDefaultBranch(repoUser, repoName, "master")
  333. if err != nil {
  334. fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
  335. }
  336. }
  337. fmt.Fprintf(out, "Processed %d references in total\n", total)
  338. _ = dWriter.Close()
  339. hookPrintResults(results)
  340. return nil
  341. }
  342. hookOptions.OldCommitIDs = oldCommitIDs[:count]
  343. hookOptions.NewCommitIDs = newCommitIDs[:count]
  344. hookOptions.RefFullNames = refFullNames[:count]
  345. fmt.Fprintf(out, " Processing %d references\n", count)
  346. resp, err := private.HookPostReceive(repoUser, repoName, hookOptions)
  347. if resp == nil {
  348. _ = dWriter.Close()
  349. hookPrintResults(results)
  350. fail("Internal Server Error", err)
  351. }
  352. wasEmpty = wasEmpty || resp.RepoWasEmpty
  353. results = append(results, resp.Results...)
  354. fmt.Fprintf(out, "Processed %d references in total\n", total)
  355. if wasEmpty && masterPushed {
  356. // We need to tell the repo to reset the default branch to master
  357. err := private.SetDefaultBranch(repoUser, repoName, "master")
  358. if err != nil {
  359. fail("Internal Server Error", "SetDefaultBranch failed with Error: %v", err)
  360. }
  361. }
  362. _ = dWriter.Close()
  363. hookPrintResults(results)
  364. return nil
  365. }
  366. func hookPrintResults(results []private.HookPostReceiveBranchResult) {
  367. for _, res := range results {
  368. if !res.Message {
  369. continue
  370. }
  371. fmt.Fprintln(os.Stderr, "")
  372. if res.Create {
  373. fmt.Fprintf(os.Stderr, "Create a new pull request for '%s':\n", res.Branch)
  374. fmt.Fprintf(os.Stderr, " %s\n", res.URL)
  375. } else {
  376. fmt.Fprint(os.Stderr, "Visit the existing pull request:\n")
  377. fmt.Fprintf(os.Stderr, " %s\n", res.URL)
  378. }
  379. fmt.Fprintln(os.Stderr, "")
  380. os.Stderr.Sync()
  381. }
  382. }