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

hook.go 11KB

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