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.

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