Skip to content

Commit 3705168

Browse files
a1012112796zeripath6543lunny
authored
Add agit flow support in gitea (#14295)
* 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]> * fix lint * simplify code add fix some nits * update merge help message * Apply suggestions from code review. Thanks @jiangxin * add forced-update message * fix lint * splite writePktLine * add refs/for/<target-branch>/<topic-branch> support also * Add test code add fix api * fix lint * fix test * skip test if git version < 2.29 * try test with git 2.30.1 * fix permission check bug * fix some nit * logic implify and test code update * fix bug * apply suggestions from code review * prepare for merge Signed-off-by: Andrew Thornton <[email protected]> * fix permission check bug - test code update - apply suggestions from code review @zeripath Signed-off-by: a1012112796 <[email protected]> * fix bug when target branch isn't exist * prevent some special push and fix some nits * fix lint * try splite * Apply suggestions from code review - fix permission check - handle user rename * fix version negotiation * remane * fix template * handle empty repo * ui: fix branch link under the title * fix nits Co-authored-by: Andrew Thornton <[email protected]> Co-authored-by: 6543 <[email protected]> Co-authored-by: Lunny Xiao <[email protected]>
1 parent 5b2e2d2 commit 3705168

File tree

30 files changed

+1334
-32
lines changed

30 files changed

+1334
-32
lines changed

cmd/hook.go

+345-1
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 {
@@ -205,6 +218,11 @@ Gitea or set your environment appropriately.`, "")
205218
}
206219
}
207220

221+
supportProcRecive := false
222+
if git.CheckGitVersionAtLeast("2.29") == nil {
223+
supportProcRecive = true
224+
}
225+
208226
for scanner.Scan() {
209227
// TODO: support news feeds for wiki
210228
if isWiki {
@@ -223,7 +241,9 @@ Gitea or set your environment appropriately.`, "")
223241
lastline++
224242

225243
// If the ref is a branch or tag, check if it's protected
226-
if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
244+
// if supportProcRecive all ref should be checked because
245+
// permission check was delayed
246+
if supportProcRecive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) {
227247
oldCommitIDs[count] = oldCommitID
228248
newCommitIDs[count] = newCommitID
229249
refFullNames[count] = refFullName
@@ -463,3 +483,327 @@ func pushOptions() map[string]string {
463483
}
464484
return opts
465485
}
486+
487+
func runHookProcReceive(c *cli.Context) error {
488+
setup("hooks/proc-receive.log", c.Bool("debug"))
489+
490+
if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 {
491+
if setting.OnlyAllowPushIfGiteaEnvironmentSet {
492+
return fail(`Rejecting changes as Gitea environment not set.
493+
If you are pushing over SSH you must push with a key managed by
494+
Gitea or set your environment appropriately.`, "")
495+
}
496+
return nil
497+
}
498+
499+
ctx, cancel := installSignals()
500+
defer cancel()
501+
502+
if git.CheckGitVersionAtLeast("2.29") != nil {
503+
return fail("Internal Server Error", "git not support proc-receive.")
504+
}
505+
506+
reader := bufio.NewReader(os.Stdin)
507+
repoUser := os.Getenv(models.EnvRepoUsername)
508+
repoName := os.Getenv(models.EnvRepoName)
509+
pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64)
510+
pusherName := os.Getenv(models.EnvPusherName)
511+
512+
// 1. Version and features negotiation.
513+
// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n)
514+
// S: flush-pkt
515+
// H: PKT-LINE(version=1\0push-options...)
516+
// H: flush-pkt
517+
518+
rs, err := readPktLine(reader, pktLineTypeData)
519+
if err != nil {
520+
return err
521+
}
522+
523+
const VersionHead string = "version=1"
524+
525+
var (
526+
hasPushOptions bool
527+
response = []byte(VersionHead)
528+
requestOptions []string
529+
)
530+
531+
index := bytes.IndexByte(rs.Data, byte(0))
532+
if index >= len(rs.Data) {
533+
return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data))
534+
}
535+
536+
if index < 0 {
537+
if len(rs.Data) == 10 && rs.Data[9] == '\n' {
538+
index = 9
539+
} else {
540+
return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data))
541+
}
542+
}
543+
544+
if string(rs.Data[0:index]) != VersionHead {
545+
return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index]))
546+
}
547+
requestOptions = strings.Split(string(rs.Data[index+1:]), " ")
548+
549+
for _, option := range requestOptions {
550+
if strings.HasPrefix(option, "push-options") {
551+
response = append(response, byte(0))
552+
response = append(response, []byte("push-options")...)
553+
hasPushOptions = true
554+
}
555+
}
556+
response = append(response, '\n')
557+
558+
_, err = readPktLine(reader, pktLineTypeFlush)
559+
if err != nil {
560+
return err
561+
}
562+
563+
err = writeDataPktLine(os.Stdout, response)
564+
if err != nil {
565+
return err
566+
}
567+
568+
err = writeFlushPktLine(os.Stdout)
569+
if err != nil {
570+
return err
571+
}
572+
573+
// 2. receive commands from server.
574+
// S: PKT-LINE(<old-oid> <new-oid> <ref>)
575+
// S: ... ...
576+
// S: flush-pkt
577+
// # [receive push-options]
578+
// S: PKT-LINE(push-option)
579+
// S: ... ...
580+
// S: flush-pkt
581+
hookOptions := private.HookOptions{
582+
UserName: pusherName,
583+
UserID: pusherID,
584+
}
585+
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
586+
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
587+
hookOptions.RefFullNames = make([]string, 0, hookBatchSize)
588+
589+
for {
590+
// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed
591+
rs, err = readPktLine(reader, pktLineTypeUnknow)
592+
if err != nil {
593+
return err
594+
}
595+
596+
if rs.Type == pktLineTypeFlush {
597+
break
598+
}
599+
t := strings.SplitN(string(rs.Data), " ", 3)
600+
if len(t) != 3 {
601+
continue
602+
}
603+
hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0])
604+
hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1])
605+
hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2])
606+
}
607+
608+
hookOptions.GitPushOptions = make(map[string]string)
609+
610+
if hasPushOptions {
611+
for {
612+
rs, err = readPktLine(reader, pktLineTypeUnknow)
613+
if err != nil {
614+
return err
615+
}
616+
617+
if rs.Type == pktLineTypeFlush {
618+
break
619+
}
620+
621+
kv := strings.SplitN(string(rs.Data), "=", 2)
622+
if len(kv) == 2 {
623+
hookOptions.GitPushOptions[kv[0]] = kv[1]
624+
}
625+
}
626+
}
627+
628+
// 3. run hook
629+
resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions)
630+
if err != nil {
631+
return fail("Internal Server Error", "run proc-receive hook failed :%v", err)
632+
}
633+
634+
// 4. response result to service
635+
// # a. OK, but has an alternate reference. The alternate reference name
636+
// # and other status can be given in option directives.
637+
// H: PKT-LINE(ok <ref>)
638+
// H: PKT-LINE(option refname <refname>)
639+
// H: PKT-LINE(option old-oid <old-oid>)
640+
// H: PKT-LINE(option new-oid <new-oid>)
641+
// H: PKT-LINE(option forced-update)
642+
// H: ... ...
643+
// H: flush-pkt
644+
// # b. NO, I reject it.
645+
// H: PKT-LINE(ng <ref> <reason>)
646+
// # c. Fall through, let 'receive-pack' to execute it.
647+
// H: PKT-LINE(ok <ref>)
648+
// H: PKT-LINE(option fall-through)
649+
650+
for _, rs := range resp.Results {
651+
if len(rs.Err) > 0 {
652+
err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err))
653+
if err != nil {
654+
return err
655+
}
656+
continue
657+
}
658+
659+
if rs.IsNotMatched {
660+
err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef))
661+
if err != nil {
662+
return err
663+
}
664+
err = writeDataPktLine(os.Stdout, []byte("option fall-through"))
665+
if err != nil {
666+
return err
667+
}
668+
continue
669+
}
670+
671+
err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef))
672+
if err != nil {
673+
return err
674+
}
675+
err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref))
676+
if err != nil {
677+
return err
678+
}
679+
if rs.OldOID != git.EmptySHA {
680+
err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID))
681+
if err != nil {
682+
return err
683+
}
684+
}
685+
err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID))
686+
if err != nil {
687+
return err
688+
}
689+
if rs.IsForcePush {
690+
err = writeDataPktLine(os.Stdout, []byte("option forced-update"))
691+
if err != nil {
692+
return err
693+
}
694+
}
695+
}
696+
err = writeFlushPktLine(os.Stdout)
697+
698+
return err
699+
}
700+
701+
// git PKT-Line api
702+
// pktLineType message type of pkt-line
703+
type pktLineType int64
704+
705+
const (
706+
// UnKnow type
707+
pktLineTypeUnknow pktLineType = 0
708+
// flush-pkt "0000"
709+
pktLineTypeFlush pktLineType = iota
710+
// data line
711+
pktLineTypeData
712+
)
713+
714+
// gitPktLine pkt-line api
715+
type gitPktLine struct {
716+
Type pktLineType
717+
Length uint64
718+
Data []byte
719+
}
720+
721+
func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) {
722+
var (
723+
err error
724+
r *gitPktLine
725+
)
726+
727+
// read prefix
728+
lengthBytes := make([]byte, 4)
729+
for i := 0; i < 4; i++ {
730+
lengthBytes[i], err = in.ReadByte()
731+
if err != nil {
732+
return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err)
733+
}
734+
}
735+
736+
r = new(gitPktLine)
737+
r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32)
738+
if err != nil {
739+
return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err)
740+
}
741+
742+
if r.Length == 0 {
743+
if requestType == pktLineTypeData {
744+
return nil, fail("Internal Server Error", "Pkt-Line format is wrong")
745+
}
746+
r.Type = pktLineTypeFlush
747+
return r, nil
748+
}
749+
750+
if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush {
751+
return nil, fail("Internal Server Error", "Pkt-Line format is wrong")
752+
}
753+
754+
r.Data = make([]byte, r.Length-4)
755+
for i := range r.Data {
756+
r.Data[i], err = in.ReadByte()
757+
if err != nil {
758+
return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err)
759+
}
760+
}
761+
762+
r.Type = pktLineTypeData
763+
764+
return r, nil
765+
}
766+
767+
func writeFlushPktLine(out io.Writer) error {
768+
l, err := out.Write([]byte("0000"))
769+
if err != nil {
770+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
771+
}
772+
if l != 4 {
773+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
774+
}
775+
776+
return nil
777+
}
778+
779+
func writeDataPktLine(out io.Writer, data []byte) error {
780+
hexchar := []byte("0123456789abcdef")
781+
hex := func(n uint64) byte {
782+
return hexchar[(n)&15]
783+
}
784+
785+
length := uint64(len(data) + 4)
786+
tmp := make([]byte, 4)
787+
tmp[0] = hex(length >> 12)
788+
tmp[1] = hex(length >> 8)
789+
tmp[2] = hex(length >> 4)
790+
tmp[3] = hex(length)
791+
792+
lr, err := out.Write(tmp)
793+
if err != nil {
794+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
795+
}
796+
if 4 != lr {
797+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
798+
}
799+
800+
lr, err = out.Write(data)
801+
if err != nil {
802+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
803+
}
804+
if int(length-4) != lr {
805+
return fail("Internal Server Error", "Pkt-Line response failed: %v", err)
806+
}
807+
808+
return nil
809+
}

0 commit comments

Comments
 (0)