Skip to content

Commit 36c77da

Browse files
committed
feature: add agit flow support
ref: https://git-repo.info/en/2020/03/agit-flow-and-git-repo/ example: ```Bash git checkout -b test echo "test" >> README.md git commit -m "test" git push origin HEAD:refs/for/master -o topic=test ``` Signed-off-by: a1012112796 <[email protected]>
1 parent 19fccdc commit 36c77da

File tree

20 files changed

+864
-22
lines changed

20 files changed

+864
-22
lines changed

cmd/hook.go

+271
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ var (
3838
subcmdHookPreReceive,
3939
subcmdHookUpdate,
4040
subcmdHookPostReceive,
41+
subcmdHookProcReceive,
4142
},
4243
}
4344

@@ -74,6 +75,18 @@ var (
7475
},
7576
},
7677
}
78+
// Note: new hook since git 2.29
79+
subcmdHookProcReceive = cli.Command{
80+
Name: "proc-receive",
81+
Usage: "Delegate proc-receive Git hook",
82+
Description: "This command should only be called by Git",
83+
Action: runHookProcReceive,
84+
Flags: []cli.Flag{
85+
cli.BoolFlag{
86+
Name: "debug",
87+
},
88+
},
89+
}
7790
)
7891

7992
type delayWriter struct {
@@ -460,3 +473,261 @@ func pushOptions() map[string]string {
460473
}
461474
return opts
462475
}
476+
477+
func runHookProcReceive(c *cli.Context) error {
478+
setup("hooks/proc-receive.log", c.Bool("debug"))
479+
480+
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
481+
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
482+
fail(`Rejecting changes as Gitea environment not set.
483+
If you are pushing over SSH you must push with a key managed by
484+
Gitea or set your environment appropriately.`, "")
485+
} else {
486+
return nil
487+
}
488+
}
489+
490+
lf, err := os.OpenFile("test.log", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0777)
491+
if err != nil {
492+
fail("Internal Server Error", "open log file failed: %v", err)
493+
}
494+
defer lf.Close()
495+
496+
if git.CheckGitVersionAtLeast("2.29") != nil {
497+
fail("Internal Server Error", "git not support proc-receive.")
498+
}
499+
500+
reader := bufio.NewReader(os.Stdin)
501+
repoUser := os.Getenv(models.EnvRepoUsername)
502+
repoName := os.Getenv(models.EnvRepoName)
503+
pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
504+
pusherName := os.Getenv(models.EnvPusherName)
505+
506+
// 1. Version and features negotiation.
507+
// S: PKT-LINE(version=1\0push-options atomic...)
508+
// S: flush-pkt
509+
// H: PKT-LINE(version=1\0push-options...)
510+
// H: flush-pkt
511+
512+
rs, err := readPktLine(reader)
513+
if err != nil {
514+
fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
515+
}
516+
if rs.Type != pktLineTypeData {
517+
fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs)
518+
}
519+
520+
const VersionHead string = "version=1"
521+
522+
if !strings.HasPrefix(rs.Data, VersionHead) {
523+
fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs)
524+
}
525+
526+
hasPushOptions := false
527+
response := []byte(VersionHead)
528+
if strings.Contains(rs.Data, "push-options") {
529+
response = append(response, byte(0))
530+
response = append(response, []byte("push-options")...)
531+
hasPushOptions = true
532+
}
533+
response = append(response, []byte("\n")...)
534+
535+
rs, err = readPktLine(reader)
536+
if err != nil {
537+
fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
538+
}
539+
if rs.Type != pktLineTypeFlush {
540+
fail("Internal Server Error", "Pkt-Line format is wrong. get %v", rs)
541+
}
542+
543+
err = writePktLine(os.Stdout, pktLineTypeData, response)
544+
if err != nil {
545+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
546+
}
547+
548+
err = writePktLine(os.Stdout, pktLineTypeFlush, nil)
549+
if err != nil {
550+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
551+
}
552+
553+
// 2. receive commands from server.
554+
// S: PKT-LINE(<old-oid> <new-oid> <ref>)
555+
// S: ... ...
556+
// S: flush-pkt
557+
// # receive push-options
558+
// S: PKT-LINE(push-option)
559+
// S: ... ...
560+
// S: flush-pkt
561+
hookOptions := private.HookOptions{
562+
UserName: pusherName,
563+
UserID: pusherID,
564+
}
565+
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
566+
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
567+
hookOptions.RefFullNames = make([]string, 0, hookBatchSize)
568+
569+
for {
570+
rs, err = readPktLine(reader)
571+
if err != nil {
572+
fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
573+
}
574+
if rs.Type == pktLineTypeFlush {
575+
break
576+
}
577+
t := strings.SplitN(rs.Data, " ", 3)
578+
if len(t) != 3 {
579+
continue
580+
}
581+
hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
582+
hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
583+
hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2])
584+
}
585+
586+
hookOptions.GitPushOptions = make(map[string]string)
587+
588+
if hasPushOptions {
589+
for {
590+
rs, err = readPktLine(reader)
591+
if err != nil {
592+
fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
593+
}
594+
if rs.Type == pktLineTypeFlush {
595+
break
596+
}
597+
598+
kv := strings.SplitN(rs.Data, "=", 2)
599+
if len(kv) == 2 {
600+
hookOptions.GitPushOptions[kv[0]] = kv[1]
601+
}
602+
}
603+
}
604+
605+
// run hook
606+
resp, err := private.HookProcReceive(repoUser, repoName, hookOptions)
607+
if err != nil {
608+
fail("Internal Server Error", "run proc-receive hook failed :%v", err)
609+
}
610+
611+
// 3 response result to service.
612+
// # OK, but has an alternate reference. The alternate reference name
613+
// # and other status can be given in option directives.
614+
// H: PKT-LINE(ok <ref>)
615+
// H: PKT-LINE(option refname <refname>)
616+
// H: PKT-LINE(option old-oid <old-oid>)
617+
// H: PKT-LINE(option new-oid <new-oid>)
618+
// H: PKT-LINE(option forced-update)
619+
// H: ... ...
620+
// H: flush-pkt
621+
for _, rs := range resp.Results {
622+
err = writePktLine(os.Stdout, pktLineTypeData, []byte("ok "+rs.OrignRef))
623+
if err != nil {
624+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
625+
}
626+
err = writePktLine(os.Stdout, pktLineTypeData, []byte("option refname "+rs.Ref))
627+
if err != nil {
628+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
629+
}
630+
err = writePktLine(os.Stdout, pktLineTypeData, []byte("option old-oid "+rs.OldOID))
631+
if err != nil {
632+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
633+
}
634+
err = writePktLine(os.Stdout, pktLineTypeData, []byte("option new-oid "+rs.NewOID))
635+
if err != nil {
636+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
637+
}
638+
}
639+
err = writePktLine(os.Stdout, pktLineTypeFlush, nil)
640+
if err != nil {
641+
fail("Internal Server Error", "Pkt-Line response failed: %v", err)
642+
}
643+
return nil
644+
}
645+
646+
// git PKT-Line api
647+
// pktLineType message type of pkt-line
648+
type pktLineType int64
649+
650+
const (
651+
// UnKnow type
652+
pktLineTypeUnknow pktLineType = 0
653+
// flush-pkt "0000"
654+
pktLineTypeFlush pktLineType = iota
655+
// data line
656+
pktLineTypeData
657+
)
658+
659+
// gitPktLine pkt-line api
660+
type gitPktLine struct {
661+
Type pktLineType
662+
Length int64
663+
Data string
664+
}
665+
666+
func readPktLine(in *bufio.Reader) (r *gitPktLine, err error) {
667+
// read prefix
668+
lengthBytes := make([]byte, 4)
669+
for i := 0; i < 4; i++ {
670+
lengthBytes[i], err = in.ReadByte()
671+
if err != nil {
672+
return nil, err
673+
}
674+
}
675+
r = new(gitPktLine)
676+
r.Length, err = strconv.ParseInt(string(lengthBytes), 16, 64)
677+
if err != nil {
678+
return nil, err
679+
}
680+
681+
if r.Length == 0 {
682+
r.Type = pktLineTypeFlush
683+
return r, nil
684+
}
685+
686+
if r.Length <= 4 || r.Length > 65520 {
687+
r.Type = pktLineTypeUnknow
688+
return r, nil
689+
}
690+
691+
tmp := make([]byte, r.Length-4)
692+
for i := range tmp {
693+
tmp[i], err = in.ReadByte()
694+
if err != nil {
695+
return nil, err
696+
}
697+
}
698+
699+
r.Type = pktLineTypeData
700+
r.Data = string(tmp)
701+
702+
return r, nil
703+
}
704+
705+
func writePktLine(out io.Writer, typ pktLineType, data []byte) error {
706+
if typ == pktLineTypeFlush {
707+
l, err := out.Write([]byte("0000"))
708+
if err != nil {
709+
return err
710+
}
711+
if l != 4 {
712+
return fmt.Errorf("real write length is different with request, want %v, real %v", 4, l)
713+
}
714+
}
715+
716+
if typ != pktLineTypeData {
717+
return nil
718+
}
719+
720+
l := len(data) + 4
721+
tmp := []byte(fmt.Sprintf("%04x", l))
722+
tmp = append(tmp, data...)
723+
724+
lr, err := out.Write(tmp)
725+
if err != nil {
726+
return err
727+
}
728+
if l != lr {
729+
return fmt.Errorf("real write length is different with request, want %v, real %v", l, lr)
730+
}
731+
732+
return nil
733+
}

cmd/hook_test.go

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// Copyright 2021 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+
5+
package cmd
6+
7+
import (
8+
"bufio"
9+
"bytes"
10+
"strings"
11+
"testing"
12+
13+
"github.com/stretchr/testify/assert"
14+
)
15+
16+
func TestPktLine(t *testing.T) {
17+
// test read
18+
s := strings.NewReader("0000")
19+
r := bufio.NewReader(s)
20+
result, err := readPktLine(r)
21+
assert.NoError(t, err)
22+
assert.Equal(t, pktLineTypeFlush, result.Type)
23+
24+
s = strings.NewReader("0006a\n")
25+
r = bufio.NewReader(s)
26+
result, err = readPktLine(r)
27+
assert.NoError(t, err)
28+
assert.Equal(t, pktLineTypeData, result.Type)
29+
assert.Equal(t, "a\n", result.Data)
30+
31+
// test write
32+
w := bytes.NewBuffer([]byte{})
33+
err = writePktLine(w, pktLineTypeFlush, nil)
34+
assert.NoError(t, err)
35+
assert.Equal(t, []byte("0000"), w.Bytes())
36+
37+
w.Reset()
38+
err = writePktLine(w, pktLineTypeData, []byte("a\nb"))
39+
assert.NoError(t, err)
40+
assert.Equal(t, []byte("0007a\nb"), w.Bytes())
41+
}

cmd/serv.go

+13
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"time"
1919

2020
"code.gitea.io/gitea/models"
21+
"code.gitea.io/gitea/modules/git"
2122
"code.gitea.io/gitea/modules/lfs"
2223
"code.gitea.io/gitea/modules/log"
2324
"code.gitea.io/gitea/modules/pprof"
@@ -135,6 +136,13 @@ func runServ(c *cli.Context) error {
135136
}
136137

137138
if len(words) < 2 {
139+
if git.CheckGitVersionAtLeast("2.29") == nil {
140+
// for AGit Flow
141+
if cmd == "ssh_info" {
142+
fmt.Print(`{"type":"gitea","version":1}`)
143+
return nil
144+
}
145+
}
138146
fail("Too few arguments", "Too few arguments in cmd: %s", cmd)
139147
}
140148

@@ -203,6 +211,11 @@ func runServ(c *cli.Context) error {
203211
}
204212
}
205213

214+
// Because of special ref "refs/for" .. , need delay write permission check
215+
if git.CheckGitVersionAtLeast("2.29") == nil {
216+
requestedMode = models.AccessModeRead
217+
}
218+
206219
results, err := private.ServCommand(keyID, username, reponame, requestedMode, verb, lfsVerb)
207220
if err != nil {
208221
if private.IsErrServCommand(err) {

models/migrations/migrations.go

+2
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,8 @@ var migrations = []Migration{
284284
NewMigration("Add user redirect", addUserRedirect),
285285
// v168 -> v169
286286
NewMigration("Recreate user table to fix default values", recreateUserTableToFixDefaultValues),
287+
// v169 -> v170
288+
NewMigration("Add agit style pull request support", addAgitStylePullRequest),
287289
}
288290

289291
// GetCurrentDBVersion returns the current db version

models/migrations/v169.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright 2021 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+
5+
package migrations
6+
7+
import (
8+
"fmt"
9+
10+
"xorm.io/xorm"
11+
)
12+
13+
func addAgitStylePullRequest(x *xorm.Engine) error {
14+
type PullRequestStyle int
15+
16+
type PullRequest struct {
17+
TopicBranch string
18+
Style PullRequestStyle
19+
}
20+
21+
if err := x.Sync2(new(PullRequest)); err != nil {
22+
return fmt.Errorf("Sync2: %v", err)
23+
}
24+
return nil
25+
}

0 commit comments

Comments
 (0)