mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 21:28:11 +09:00 
			
		
		
		
	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 <1012112796@qq.com> * 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 <art27@cantab.net> * fix permission check bug - test code update - apply suggestions from code review @zeripath Signed-off-by: a1012112796 <1012112796@qq.com> * 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 <art27@cantab.net> Co-authored-by: 6543 <6543@obermui.de> Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>
This commit is contained in:
		
							
								
								
									
										346
									
								
								cmd/hook.go
									
									
									
									
									
								
							
							
						
						
									
										346
									
								
								cmd/hook.go
									
									
									
									
									
								
							| @@ -38,6 +38,7 @@ var ( | ||||
| 			subcmdHookPreReceive, | ||||
| 			subcmdHookUpdate, | ||||
| 			subcmdHookPostReceive, | ||||
| 			subcmdHookProcReceive, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| @@ -74,6 +75,18 @@ var ( | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 	// Note: new hook since git 2.29 | ||||
| 	subcmdHookProcReceive = cli.Command{ | ||||
| 		Name:        "proc-receive", | ||||
| 		Usage:       "Delegate proc-receive Git hook", | ||||
| 		Description: "This command should only be called by Git", | ||||
| 		Action:      runHookProcReceive, | ||||
| 		Flags: []cli.Flag{ | ||||
| 			cli.BoolFlag{ | ||||
| 				Name: "debug", | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| ) | ||||
|  | ||||
| type delayWriter struct { | ||||
| @@ -205,6 +218,11 @@ Gitea or set your environment appropriately.`, "") | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	supportProcRecive := false | ||||
| 	if git.CheckGitVersionAtLeast("2.29") == nil { | ||||
| 		supportProcRecive = true | ||||
| 	} | ||||
|  | ||||
| 	for scanner.Scan() { | ||||
| 		// TODO: support news feeds for wiki | ||||
| 		if isWiki { | ||||
| @@ -223,7 +241,9 @@ Gitea or set your environment appropriately.`, "") | ||||
| 		lastline++ | ||||
|  | ||||
| 		// If the ref is a branch or tag, check if it's protected | ||||
| 		if strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { | ||||
| 		// if supportProcRecive all ref should be checked because | ||||
| 		// permission check was delayed | ||||
| 		if supportProcRecive || strings.HasPrefix(refFullName, git.BranchPrefix) || strings.HasPrefix(refFullName, git.TagPrefix) { | ||||
| 			oldCommitIDs[count] = oldCommitID | ||||
| 			newCommitIDs[count] = newCommitID | ||||
| 			refFullNames[count] = refFullName | ||||
| @@ -463,3 +483,327 @@ func pushOptions() map[string]string { | ||||
| 	} | ||||
| 	return opts | ||||
| } | ||||
|  | ||||
| func runHookProcReceive(c *cli.Context) error { | ||||
| 	setup("hooks/proc-receive.log", c.Bool("debug")) | ||||
|  | ||||
| 	if len(os.Getenv("SSH_ORIGINAL_COMMAND")) == 0 { | ||||
| 		if setting.OnlyAllowPushIfGiteaEnvironmentSet { | ||||
| 			return fail(`Rejecting changes as Gitea environment not set. | ||||
| If you are pushing over SSH you must push with a key managed by | ||||
| Gitea or set your environment appropriately.`, "") | ||||
| 		} | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	ctx, cancel := installSignals() | ||||
| 	defer cancel() | ||||
|  | ||||
| 	if git.CheckGitVersionAtLeast("2.29") != nil { | ||||
| 		return fail("Internal Server Error", "git not support proc-receive.") | ||||
| 	} | ||||
|  | ||||
| 	reader := bufio.NewReader(os.Stdin) | ||||
| 	repoUser := os.Getenv(models.EnvRepoUsername) | ||||
| 	repoName := os.Getenv(models.EnvRepoName) | ||||
| 	pusherID, _ := strconv.ParseInt(os.Getenv(models.EnvPusherID), 10, 64) | ||||
| 	pusherName := os.Getenv(models.EnvPusherName) | ||||
|  | ||||
| 	// 1. Version and features negotiation. | ||||
| 	// S: PKT-LINE(version=1\0push-options atomic...) / PKT-LINE(version=1\n) | ||||
| 	// S: flush-pkt | ||||
| 	// H: PKT-LINE(version=1\0push-options...) | ||||
| 	// H: flush-pkt | ||||
|  | ||||
| 	rs, err := readPktLine(reader, pktLineTypeData) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	const VersionHead string = "version=1" | ||||
|  | ||||
| 	var ( | ||||
| 		hasPushOptions bool | ||||
| 		response       = []byte(VersionHead) | ||||
| 		requestOptions []string | ||||
| 	) | ||||
|  | ||||
| 	index := bytes.IndexByte(rs.Data, byte(0)) | ||||
| 	if index >= len(rs.Data) { | ||||
| 		return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) | ||||
| 	} | ||||
|  | ||||
| 	if index < 0 { | ||||
| 		if len(rs.Data) == 10 && rs.Data[9] == '\n' { | ||||
| 			index = 9 | ||||
| 		} else { | ||||
| 			return fail("Internal Server Error", "pkt-line: format error "+fmt.Sprint(rs.Data)) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if string(rs.Data[0:index]) != VersionHead { | ||||
| 		return fail("Internal Server Error", "Received unsupported version: %s", string(rs.Data[0:index])) | ||||
| 	} | ||||
| 	requestOptions = strings.Split(string(rs.Data[index+1:]), " ") | ||||
|  | ||||
| 	for _, option := range requestOptions { | ||||
| 		if strings.HasPrefix(option, "push-options") { | ||||
| 			response = append(response, byte(0)) | ||||
| 			response = append(response, []byte("push-options")...) | ||||
| 			hasPushOptions = true | ||||
| 		} | ||||
| 	} | ||||
| 	response = append(response, '\n') | ||||
|  | ||||
| 	_, err = readPktLine(reader, pktLineTypeFlush) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = writeDataPktLine(os.Stdout, response) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	err = writeFlushPktLine(os.Stdout) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	// 2. receive commands from server. | ||||
| 	// S: PKT-LINE(<old-oid> <new-oid> <ref>) | ||||
| 	// S: ... ... | ||||
| 	// S: flush-pkt | ||||
| 	// # [receive push-options] | ||||
| 	// S: PKT-LINE(push-option) | ||||
| 	// S: ... ... | ||||
| 	// S: flush-pkt | ||||
| 	hookOptions := private.HookOptions{ | ||||
| 		UserName: pusherName, | ||||
| 		UserID:   pusherID, | ||||
| 	} | ||||
| 	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) | ||||
| 	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) | ||||
| 	hookOptions.RefFullNames = make([]string, 0, hookBatchSize) | ||||
|  | ||||
| 	for { | ||||
| 		// note: pktLineTypeUnknow means pktLineTypeFlush and pktLineTypeData all allowed | ||||
| 		rs, err = readPktLine(reader, pktLineTypeUnknow) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if rs.Type == pktLineTypeFlush { | ||||
| 			break | ||||
| 		} | ||||
| 		t := strings.SplitN(string(rs.Data), " ", 3) | ||||
| 		if len(t) != 3 { | ||||
| 			continue | ||||
| 		} | ||||
| 		hookOptions.OldCommitIDs = append(hookOptions.OldCommitIDs, t[0]) | ||||
| 		hookOptions.NewCommitIDs = append(hookOptions.NewCommitIDs, t[1]) | ||||
| 		hookOptions.RefFullNames = append(hookOptions.RefFullNames, t[2]) | ||||
| 	} | ||||
|  | ||||
| 	hookOptions.GitPushOptions = make(map[string]string) | ||||
|  | ||||
| 	if hasPushOptions { | ||||
| 		for { | ||||
| 			rs, err = readPktLine(reader, pktLineTypeUnknow) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if rs.Type == pktLineTypeFlush { | ||||
| 				break | ||||
| 			} | ||||
|  | ||||
| 			kv := strings.SplitN(string(rs.Data), "=", 2) | ||||
| 			if len(kv) == 2 { | ||||
| 				hookOptions.GitPushOptions[kv[0]] = kv[1] | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 3. run hook | ||||
| 	resp, err := private.HookProcReceive(ctx, repoUser, repoName, hookOptions) | ||||
| 	if err != nil { | ||||
| 		return fail("Internal Server Error", "run proc-receive hook failed :%v", err) | ||||
| 	} | ||||
|  | ||||
| 	// 4. response result to service | ||||
| 	// # a. OK, but has an alternate reference.  The alternate reference name | ||||
| 	// # and other status can be given in option directives. | ||||
| 	// H: PKT-LINE(ok <ref>) | ||||
| 	// H: PKT-LINE(option refname <refname>) | ||||
| 	// H: PKT-LINE(option old-oid <old-oid>) | ||||
| 	// H: PKT-LINE(option new-oid <new-oid>) | ||||
| 	// H: PKT-LINE(option forced-update) | ||||
| 	// H: ... ... | ||||
| 	// H: flush-pkt | ||||
| 	// # b. NO, I reject it. | ||||
| 	// H: PKT-LINE(ng <ref> <reason>) | ||||
| 	// # c. Fall through, let 'receive-pack' to execute it. | ||||
| 	// H: PKT-LINE(ok <ref>) | ||||
| 	// H: PKT-LINE(option fall-through) | ||||
|  | ||||
| 	for _, rs := range resp.Results { | ||||
| 		if len(rs.Err) > 0 { | ||||
| 			err = writeDataPktLine(os.Stdout, []byte("ng "+rs.OriginalRef+" "+rs.Err)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if rs.IsNotMatched { | ||||
| 			err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			err = writeDataPktLine(os.Stdout, []byte("option fall-through")) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		err = writeDataPktLine(os.Stdout, []byte("ok "+rs.OriginalRef)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		err = writeDataPktLine(os.Stdout, []byte("option refname "+rs.Ref)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if rs.OldOID != git.EmptySHA { | ||||
| 			err = writeDataPktLine(os.Stdout, []byte("option old-oid "+rs.OldOID)) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 		err = writeDataPktLine(os.Stdout, []byte("option new-oid "+rs.NewOID)) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		if rs.IsForcePush { | ||||
| 			err = writeDataPktLine(os.Stdout, []byte("option forced-update")) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	err = writeFlushPktLine(os.Stdout) | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| // git PKT-Line api | ||||
| // pktLineType message type of pkt-line | ||||
| type pktLineType int64 | ||||
|  | ||||
| const ( | ||||
| 	// UnKnow type | ||||
| 	pktLineTypeUnknow pktLineType = 0 | ||||
| 	// flush-pkt "0000" | ||||
| 	pktLineTypeFlush pktLineType = iota | ||||
| 	// data line | ||||
| 	pktLineTypeData | ||||
| ) | ||||
|  | ||||
| // gitPktLine pkt-line api | ||||
| type gitPktLine struct { | ||||
| 	Type   pktLineType | ||||
| 	Length uint64 | ||||
| 	Data   []byte | ||||
| } | ||||
|  | ||||
| func readPktLine(in *bufio.Reader, requestType pktLineType) (*gitPktLine, error) { | ||||
| 	var ( | ||||
| 		err error | ||||
| 		r   *gitPktLine | ||||
| 	) | ||||
|  | ||||
| 	// read prefix | ||||
| 	lengthBytes := make([]byte, 4) | ||||
| 	for i := 0; i < 4; i++ { | ||||
| 		lengthBytes[i], err = in.ReadByte() | ||||
| 		if err != nil { | ||||
| 			return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	r = new(gitPktLine) | ||||
| 	r.Length, err = strconv.ParseUint(string(lengthBytes), 16, 32) | ||||
| 	if err != nil { | ||||
| 		return nil, fail("Internal Server Error", "Pkt-Line format is wrong :%v", err) | ||||
| 	} | ||||
|  | ||||
| 	if r.Length == 0 { | ||||
| 		if requestType == pktLineTypeData { | ||||
| 			return nil, fail("Internal Server Error", "Pkt-Line format is wrong") | ||||
| 		} | ||||
| 		r.Type = pktLineTypeFlush | ||||
| 		return r, nil | ||||
| 	} | ||||
|  | ||||
| 	if r.Length <= 4 || r.Length > 65520 || requestType == pktLineTypeFlush { | ||||
| 		return nil, fail("Internal Server Error", "Pkt-Line format is wrong") | ||||
| 	} | ||||
|  | ||||
| 	r.Data = make([]byte, r.Length-4) | ||||
| 	for i := range r.Data { | ||||
| 		r.Data[i], err = in.ReadByte() | ||||
| 		if err != nil { | ||||
| 			return nil, fail("Internal Server Error", "Pkt-Line: read stdin failed : %v", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	r.Type = pktLineTypeData | ||||
|  | ||||
| 	return r, nil | ||||
| } | ||||
|  | ||||
| func writeFlushPktLine(out io.Writer) error { | ||||
| 	l, err := out.Write([]byte("0000")) | ||||
| 	if err != nil { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
| 	if l != 4 { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func writeDataPktLine(out io.Writer, data []byte) error { | ||||
| 	hexchar := []byte("0123456789abcdef") | ||||
| 	hex := func(n uint64) byte { | ||||
| 		return hexchar[(n)&15] | ||||
| 	} | ||||
|  | ||||
| 	length := uint64(len(data) + 4) | ||||
| 	tmp := make([]byte, 4) | ||||
| 	tmp[0] = hex(length >> 12) | ||||
| 	tmp[1] = hex(length >> 8) | ||||
| 	tmp[2] = hex(length >> 4) | ||||
| 	tmp[3] = hex(length) | ||||
|  | ||||
| 	lr, err := out.Write(tmp) | ||||
| 	if err != nil { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
| 	if 4 != lr { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	lr, err = out.Write(data) | ||||
| 	if err != nil { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
| 	if int(length-4) != lr { | ||||
| 		return fail("Internal Server Error", "Pkt-Line response failed: %v", err) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										41
									
								
								cmd/hook_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								cmd/hook_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package cmd | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"bytes" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestPktLine(t *testing.T) { | ||||
| 	// test read | ||||
| 	s := strings.NewReader("0000") | ||||
| 	r := bufio.NewReader(s) | ||||
| 	result, err := readPktLine(r, pktLineTypeFlush) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, pktLineTypeFlush, result.Type) | ||||
|  | ||||
| 	s = strings.NewReader("0006a\n") | ||||
| 	r = bufio.NewReader(s) | ||||
| 	result, err = readPktLine(r, pktLineTypeData) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, pktLineTypeData, result.Type) | ||||
| 	assert.Equal(t, []byte("a\n"), result.Data) | ||||
|  | ||||
| 	// test write | ||||
| 	w := bytes.NewBuffer([]byte{}) | ||||
| 	err = writeFlushPktLine(w) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, []byte("0000"), w.Bytes()) | ||||
|  | ||||
| 	w.Reset() | ||||
| 	err = writeDataPktLine(w, []byte("a\nb")) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, []byte("0007a\nb"), w.Bytes()) | ||||
| } | ||||
| @@ -17,6 +17,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/pprof" | ||||
| @@ -146,6 +147,13 @@ func runServ(c *cli.Context) error { | ||||
| 	} | ||||
|  | ||||
| 	if len(words) < 2 { | ||||
| 		if git.CheckGitVersionAtLeast("2.29") == nil { | ||||
| 			// for AGit Flow | ||||
| 			if cmd == "ssh_info" { | ||||
| 				fmt.Print(`{"type":"gitea","version":1}`) | ||||
| 				return nil | ||||
| 			} | ||||
| 		} | ||||
| 		return fail("Too few arguments", "Too few arguments in cmd: %s", cmd) | ||||
| 	} | ||||
|  | ||||
|   | ||||
| @@ -70,6 +70,7 @@ func testGit(t *testing.T, u *url.URL) { | ||||
| 		rawTest(t, &httpContext, little, big, littleLFS, bigLFS) | ||||
| 		mediaTest(t, &httpContext, little, big, littleLFS, bigLFS) | ||||
|  | ||||
| 		t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &httpContext, "master", "test/head")) | ||||
| 		t.Run("BranchProtectMerge", doBranchProtectPRMerge(&httpContext, dstPath)) | ||||
| 		t.Run("CreatePRAndSetManuallyMerged", doCreatePRAndSetManuallyMerged(httpContext, httpContext, dstPath, "master", "test-manually-merge")) | ||||
| 		t.Run("MergeFork", func(t *testing.T) { | ||||
| @@ -111,6 +112,7 @@ func testGit(t *testing.T, u *url.URL) { | ||||
| 			rawTest(t, &sshContext, little, big, littleLFS, bigLFS) | ||||
| 			mediaTest(t, &sshContext, little, big, littleLFS, bigLFS) | ||||
|  | ||||
| 			t.Run("CreateAgitFlowPull", doCreateAgitFlowPull(dstPath, &sshContext, "master", "test/head2")) | ||||
| 			t.Run("BranchProtectMerge", doBranchProtectPRMerge(&sshContext, dstPath)) | ||||
| 			t.Run("MergeFork", func(t *testing.T) { | ||||
| 				defer PrintCurrentTest(t)() | ||||
| @@ -593,3 +595,162 @@ func doBranchDelete(ctx APITestContext, owner, repo, branch string) func(*testin | ||||
| 		ctx.Session.MakeRequest(t, req, http.StatusOK) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func doCreateAgitFlowPull(dstPath string, ctx *APITestContext, baseBranch, headBranch string) func(t *testing.T) { | ||||
| 	return func(t *testing.T) { | ||||
| 		defer PrintCurrentTest(t)() | ||||
|  | ||||
| 		// skip this test if git version is low | ||||
| 		if git.CheckGitVersionAtLeast("2.29") != nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		gitRepo, err := git.OpenRepository(dstPath) | ||||
| 		if !assert.NoError(t, err) { | ||||
| 			return | ||||
| 		} | ||||
| 		defer gitRepo.Close() | ||||
|  | ||||
| 		var ( | ||||
| 			pr1, pr2 *models.PullRequest | ||||
| 			commit   string | ||||
| 		) | ||||
| 		repo, err := models.GetRepositoryByOwnerAndName(ctx.Username, ctx.Reponame) | ||||
| 		if !assert.NoError(t, err) { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		pullNum := models.GetCount(t, &models.PullRequest{}) | ||||
|  | ||||
| 		t.Run("CreateHeadBranch", doGitCreateBranch(dstPath, headBranch)) | ||||
|  | ||||
| 		t.Run("AddCommit", func(t *testing.T) { | ||||
| 			err := ioutil.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content"), 0666) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			err = git.AddChanges(dstPath, true) | ||||
| 			assert.NoError(t, err) | ||||
|  | ||||
| 			err = git.CommitChanges(dstPath, git.CommitChangesOptions{ | ||||
| 				Committer: &git.Signature{ | ||||
| 					Email: "user2@example.com", | ||||
| 					Name:  "user2", | ||||
| 					When:  time.Now(), | ||||
| 				}, | ||||
| 				Author: &git.Signature{ | ||||
| 					Email: "user2@example.com", | ||||
| 					Name:  "user2", | ||||
| 					When:  time.Now(), | ||||
| 				}, | ||||
| 				Message: "Testing commit 1", | ||||
| 			}) | ||||
| 			assert.NoError(t, err) | ||||
| 			commit, err = gitRepo.GetRefCommitID("HEAD") | ||||
| 			assert.NoError(t, err) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("Push", func(t *testing.T) { | ||||
| 			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			models.AssertCount(t, &models.PullRequest{}, pullNum+1) | ||||
| 			pr1 = models.AssertExistsAndLoadBean(t, &models.PullRequest{ | ||||
| 				HeadRepoID: repo.ID, | ||||
| 				Flow:       models.PullRequestFlowAGit, | ||||
| 			}).(*models.PullRequest) | ||||
| 			if !assert.NotEmpty(t, pr1) { | ||||
| 				return | ||||
| 			} | ||||
| 			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equal(t, "user2/"+headBranch, pr1.HeadBranch) | ||||
| 			assert.Equal(t, false, prMsg.HasMerged) | ||||
| 			assert.Contains(t, "Testing commit 1", prMsg.Body) | ||||
| 			assert.Equal(t, commit, prMsg.Head.Sha) | ||||
|  | ||||
| 			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			models.AssertCount(t, &models.PullRequest{}, pullNum+2) | ||||
| 			pr2 = models.AssertExistsAndLoadBean(t, &models.PullRequest{ | ||||
| 				HeadRepoID: repo.ID, | ||||
| 				Index:      pr1.Index + 1, | ||||
| 				Flow:       models.PullRequestFlowAGit, | ||||
| 			}).(*models.PullRequest) | ||||
| 			if !assert.NotEmpty(t, pr2) { | ||||
| 				return | ||||
| 			} | ||||
| 			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equal(t, "user2/test/"+headBranch, pr2.HeadBranch) | ||||
| 			assert.Equal(t, false, prMsg.HasMerged) | ||||
| 		}) | ||||
|  | ||||
| 		if pr1 == nil || pr2 == nil { | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		t.Run("AddCommit2", func(t *testing.T) { | ||||
| 			err := ioutil.WriteFile(path.Join(dstPath, "test_file"), []byte("## test content \n ## test content 2"), 0666) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			err = git.AddChanges(dstPath, true) | ||||
| 			assert.NoError(t, err) | ||||
|  | ||||
| 			err = git.CommitChanges(dstPath, git.CommitChangesOptions{ | ||||
| 				Committer: &git.Signature{ | ||||
| 					Email: "user2@example.com", | ||||
| 					Name:  "user2", | ||||
| 					When:  time.Now(), | ||||
| 				}, | ||||
| 				Author: &git.Signature{ | ||||
| 					Email: "user2@example.com", | ||||
| 					Name:  "user2", | ||||
| 					When:  time.Now(), | ||||
| 				}, | ||||
| 				Message: "Testing commit 2", | ||||
| 			}) | ||||
| 			assert.NoError(t, err) | ||||
| 			commit, err = gitRepo.GetRefCommitID("HEAD") | ||||
| 			assert.NoError(t, err) | ||||
| 		}) | ||||
|  | ||||
| 		t.Run("Push2", func(t *testing.T) { | ||||
| 			_, err := git.NewCommand("push", "origin", "HEAD:refs/for/master", "-o", "topic="+headBranch).RunInDir(dstPath) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			models.AssertCount(t, &models.PullRequest{}, pullNum+2) | ||||
| 			prMsg, err := doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)(t) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equal(t, false, prMsg.HasMerged) | ||||
| 			assert.Equal(t, commit, prMsg.Head.Sha) | ||||
|  | ||||
| 			_, err = git.NewCommand("push", "origin", "HEAD:refs/for/master/test/"+headBranch).RunInDir(dstPath) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			models.AssertCount(t, &models.PullRequest{}, pullNum+2) | ||||
| 			prMsg, err = doAPIGetPullRequest(*ctx, ctx.Username, ctx.Reponame, pr2.Index)(t) | ||||
| 			if !assert.NoError(t, err) { | ||||
| 				return | ||||
| 			} | ||||
| 			assert.Equal(t, false, prMsg.HasMerged) | ||||
| 			assert.Equal(t, commit, prMsg.Head.Sha) | ||||
| 		}) | ||||
| 		t.Run("Merge", doAPIMergePullRequest(*ctx, ctx.Username, ctx.Reponame, pr1.Index)) | ||||
| 		t.Run("CheckoutMasterAgain", doGitCheckoutBranch(dstPath, "master")) | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -329,6 +329,8 @@ var migrations = []Migration{ | ||||
| 	NewMigration("Add key is verified to gpg key", addKeyIsVerified), | ||||
| 	// v189 -> v190 | ||||
| 	NewMigration("Unwrap ldap.Sources", unwrapLDAPSourceCfg), | ||||
| 	// v190 -> v191 | ||||
| 	NewMigration("Add agit flow pull request support", addAgitFlowPullRequest), | ||||
| } | ||||
|  | ||||
| // GetCurrentDBVersion returns the current db version | ||||
|   | ||||
							
								
								
									
										24
									
								
								models/migrations/v190.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								models/migrations/v190.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package migrations | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
|  | ||||
| 	"xorm.io/xorm" | ||||
| ) | ||||
|  | ||||
| func addAgitFlowPullRequest(x *xorm.Engine) error { | ||||
| 	type PullRequestFlow int | ||||
|  | ||||
| 	type PullRequest struct { | ||||
| 		Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` | ||||
| 	} | ||||
|  | ||||
| 	if err := x.Sync2(new(PullRequest)); err != nil { | ||||
| 		return fmt.Errorf("sync2: %v", err) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @@ -38,6 +38,16 @@ const ( | ||||
| 	PullRequestStatusEmpty | ||||
| ) | ||||
|  | ||||
| // PullRequestFlow the flow of pull request | ||||
| type PullRequestFlow int | ||||
|  | ||||
| const ( | ||||
| 	// PullRequestFlowGithub github flow from head branch to base branch | ||||
| 	PullRequestFlowGithub PullRequestFlow = iota | ||||
| 	// PullRequestFlowAGit Agit flow pull request, head branch is not exist | ||||
| 	PullRequestFlowAGit | ||||
| ) | ||||
|  | ||||
| // PullRequest represents relation between pull request and repositories. | ||||
| type PullRequest struct { | ||||
| 	ID              int64 `xorm:"pk autoincr"` | ||||
| @@ -58,6 +68,7 @@ type PullRequest struct { | ||||
| 	BaseRepoID      int64       `xorm:"INDEX"` | ||||
| 	BaseRepo        *Repository `xorm:"-"` | ||||
| 	HeadBranch      string | ||||
| 	HeadCommitID    string `xorm:"-"` | ||||
| 	BaseBranch      string | ||||
| 	ProtectedBranch *ProtectedBranch `xorm:"-"` | ||||
| 	MergeBase       string           `xorm:"VARCHAR(40)"` | ||||
| @@ -69,6 +80,8 @@ type PullRequest struct { | ||||
| 	MergedUnix     timeutil.TimeStamp `xorm:"updated INDEX"` | ||||
|  | ||||
| 	isHeadRepoLoaded bool `xorm:"-"` | ||||
|  | ||||
| 	Flow PullRequestFlow `xorm:"NOT NULL DEFAULT 0"` | ||||
| } | ||||
|  | ||||
| // MustHeadUserName returns the HeadRepo's username if failed return blank | ||||
| @@ -470,11 +483,11 @@ func NewPullRequest(repo *Repository, issue *Issue, labelIDs []int64, uuids []st | ||||
|  | ||||
| // GetUnmergedPullRequest returns a pull request that is open and has not been merged | ||||
| // by given head/base and repo/branch. | ||||
| func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string) (*PullRequest, error) { | ||||
| func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch string, flow PullRequestFlow) (*PullRequest, error) { | ||||
| 	pr := new(PullRequest) | ||||
| 	has, err := x. | ||||
| 		Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND issue.is_closed=?", | ||||
| 			headRepoID, headBranch, baseRepoID, baseBranch, false, false). | ||||
| 		Where("head_repo_id=? AND head_branch=? AND base_repo_id=? AND base_branch=? AND has_merged=? AND flow = ? AND issue.is_closed=?", | ||||
| 			headRepoID, headBranch, baseRepoID, baseBranch, false, flow, false). | ||||
| 		Join("INNER", "issue", "issue.id=pull_request.issue_id"). | ||||
| 		Get(pr) | ||||
| 	if err != nil { | ||||
| @@ -491,7 +504,7 @@ func GetUnmergedPullRequest(headRepoID, baseRepoID int64, headBranch, baseBranch | ||||
| func GetLatestPullRequestByHeadInfo(repoID int64, branch string) (*PullRequest, error) { | ||||
| 	pr := new(PullRequest) | ||||
| 	has, err := x. | ||||
| 		Where("head_repo_id = ? AND head_branch = ?", repoID, branch). | ||||
| 		Where("head_repo_id = ? AND head_branch = ? AND flow = ?", repoID, branch, PullRequestFlowGithub). | ||||
| 		OrderBy("id DESC"). | ||||
| 		Get(pr) | ||||
| 	if !has { | ||||
| @@ -566,6 +579,20 @@ func getPullRequestByIssueID(e Engine, issueID int64) (*PullRequest, error) { | ||||
| 	return pr, pr.loadAttributes(e) | ||||
| } | ||||
|  | ||||
| // GetAllUnmergedAgitPullRequestByPoster get all unmerged agit flow pull request | ||||
| // By poster id. | ||||
| func GetAllUnmergedAgitPullRequestByPoster(uid int64) ([]*PullRequest, error) { | ||||
| 	pulls := make([]*PullRequest, 0, 10) | ||||
|  | ||||
| 	err := x. | ||||
| 		Where("has_merged=? AND flow = ? AND issue.is_closed=? AND issue.poster_id=?", | ||||
| 			false, PullRequestFlowAGit, false, uid). | ||||
| 		Join("INNER", "issue", "issue.id=pull_request.issue_id"). | ||||
| 		Find(&pulls) | ||||
|  | ||||
| 	return pulls, err | ||||
| } | ||||
|  | ||||
| // GetPullRequestByIssueID returns pull request by given issue ID. | ||||
| func GetPullRequestByIssueID(issueID int64) (*PullRequest, error) { | ||||
| 	return getPullRequestByIssueID(x, issueID) | ||||
| @@ -663,6 +690,10 @@ func (pr *PullRequest) GetBaseBranchHTMLURL() string { | ||||
|  | ||||
| // GetHeadBranchHTMLURL returns the HTML URL of the head branch | ||||
| func (pr *PullRequest) GetHeadBranchHTMLURL() string { | ||||
| 	if pr.Flow == PullRequestFlowAGit { | ||||
| 		return "" | ||||
| 	} | ||||
|  | ||||
| 	if err := pr.LoadHeadRepo(); err != nil { | ||||
| 		log.Error("LoadHeadRepo: %v", err) | ||||
| 		return "" | ||||
|   | ||||
| @@ -51,8 +51,8 @@ func listPullRequestStatement(baseRepoID int64, opts *PullRequestsOptions) (*xor | ||||
| func GetUnmergedPullRequestsByHeadInfo(repoID int64, branch string) ([]*PullRequest, error) { | ||||
| 	prs := make([]*PullRequest, 0, 2) | ||||
| 	return prs, x. | ||||
| 		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ?", | ||||
| 			repoID, branch, false, false). | ||||
| 		Where("head_repo_id = ? AND head_branch = ? AND has_merged = ? AND issue.is_closed = ? AND flow = ?", | ||||
| 			repoID, branch, false, false, PullRequestFlowGithub). | ||||
| 		Join("INNER", "issue", "issue.id = pull_request.issue_id"). | ||||
| 		Find(&prs) | ||||
| } | ||||
|   | ||||
| @@ -92,11 +92,11 @@ func TestPullRequestsOldest(t *testing.T) { | ||||
|  | ||||
| func TestGetUnmergedPullRequest(t *testing.T) { | ||||
| 	assert.NoError(t, PrepareTestDatabase()) | ||||
| 	pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master") | ||||
| 	pr, err := GetUnmergedPullRequest(1, 1, "branch2", "master", PullRequestFlowGithub) | ||||
| 	assert.NoError(t, err) | ||||
| 	assert.Equal(t, int64(2), pr.ID) | ||||
|  | ||||
| 	_, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master") | ||||
| 	_, err = GetUnmergedPullRequest(1, 9223372036854775807, "branch1", "master", PullRequestFlowGithub) | ||||
| 	assert.Error(t, err) | ||||
| 	assert.True(t, IsErrPullRequestNotExist(err)) | ||||
| } | ||||
|   | ||||
| @@ -95,7 +95,25 @@ func ToAPIPullRequest(pr *models.PullRequest) *api.PullRequest { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if pr.HeadRepo != nil { | ||||
| 	if pr.Flow == models.PullRequestFlowAGit { | ||||
| 		gitRepo, err := git.OpenRepository(pr.BaseRepo.RepoPath()) | ||||
| 		if err != nil { | ||||
| 			log.Error("OpenRepository[%s]: %v", pr.GetGitRefName(), err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		defer gitRepo.Close() | ||||
|  | ||||
| 		apiPullRequest.Head.Sha, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 		if err != nil { | ||||
| 			log.Error("GetRefCommitID[%s]: %v", pr.GetGitRefName(), err) | ||||
| 			return nil | ||||
| 		} | ||||
| 		apiPullRequest.Head.RepoID = pr.BaseRepoID | ||||
| 		apiPullRequest.Head.Repository = apiPullRequest.Base.Repository | ||||
| 		apiPullRequest.Head.Name = "" | ||||
| 	} | ||||
|  | ||||
| 	if pr.HeadRepo != nil && pr.Flow == models.PullRequestFlowGithub { | ||||
| 		apiPullRequest.Head.RepoID = pr.HeadRepo.ID | ||||
| 		apiPullRequest.Head.Repository = ToRepo(pr.HeadRepo, models.AccessModeNone) | ||||
|  | ||||
|   | ||||
| @@ -37,6 +37,9 @@ var ( | ||||
|  | ||||
| 	// will be checked on Init | ||||
| 	goVersionLessThan115 = true | ||||
|  | ||||
| 	// SupportProcReceive version >= 2.29.0 | ||||
| 	SupportProcReceive bool | ||||
| ) | ||||
|  | ||||
| // LocalVersion returns current Git version from shell. | ||||
| @@ -183,6 +186,19 @@ func Init(ctx context.Context) error { | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if CheckGitVersionAtLeast("2.29") == nil { | ||||
| 		// set support for AGit flow | ||||
| 		if err := checkAndAddConfig("receive.procReceiveRefs", "refs/for"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		SupportProcReceive = true | ||||
| 	} else { | ||||
| 		if err := checkAndRemoveConfig("receive.procReceiveRefs", "refs/for"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		SupportProcReceive = false | ||||
| 	} | ||||
|  | ||||
| 	if runtime.GOOS == "windows" { | ||||
| 		if err := checkAndSetConfig("core.longpaths", "true", true); err != nil { | ||||
| 			return err | ||||
| @@ -232,6 +248,51 @@ func checkAndSetConfig(key, defaultValue string, forceToDefault bool) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func checkAndAddConfig(key, value string) error { | ||||
| 	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) | ||||
| 	if err != nil { | ||||
| 		perr, ok := err.(*process.Error) | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) | ||||
| 		} | ||||
| 		eerr, ok := perr.Err.(*exec.ExitError) | ||||
| 		if !ok || eerr.ExitCode() != 1 { | ||||
| 			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) | ||||
| 		} | ||||
| 		if eerr.ExitCode() == 1 { | ||||
| 			if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--add", key, value); err != nil { | ||||
| 				return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) | ||||
| 			} | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func checkAndRemoveConfig(key, value string) error { | ||||
| 	_, stderr, err := process.GetManager().Exec("git.Init(get setting)", GitExecutable, "config", "--get", key, value) | ||||
| 	if err != nil { | ||||
| 		perr, ok := err.(*process.Error) | ||||
| 		if !ok { | ||||
| 			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) | ||||
| 		} | ||||
| 		eerr, ok := perr.Err.(*exec.ExitError) | ||||
| 		if !ok || eerr.ExitCode() != 1 { | ||||
| 			return fmt.Errorf("Failed to get git %s(%v) errType %T: %s", key, err, err, stderr) | ||||
| 		} | ||||
| 		if eerr.ExitCode() == 1 { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if _, stderr, err = process.GetManager().Exec(fmt.Sprintf("git.Init(set %s)", key), "git", "config", "--global", "--unset-all", key, value); err != nil { | ||||
| 		return fmt.Errorf("Failed to set git %s(%s): %s", key, err, stderr) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // Fsck verifies the connectivity and validity of the objects in the database | ||||
| func Fsck(ctx context.Context, repoPath string, timeout time.Duration, args ...string) error { | ||||
| 	// Make sure timeout makes sense. | ||||
|   | ||||
| @@ -13,6 +13,14 @@ import ( | ||||
| // BranchPrefix base dir of the branch information file store on git | ||||
| const BranchPrefix = "refs/heads/" | ||||
|  | ||||
| // AGit Flow | ||||
|  | ||||
| // PullRequestPrefix sepcial ref to create a pull request: refs/for/<targe-branch>/<topic-branch> | ||||
| // or refs/for/<targe-branch> -o topic='<topic-branch>' | ||||
| const PullRequestPrefix = "refs/for/" | ||||
|  | ||||
| // TODO: /refs/for-review for suggest change interface | ||||
|  | ||||
| // IsReferenceExist returns true if given reference exists in the repository. | ||||
| func IsReferenceExist(repoPath, name string) bool { | ||||
| 	_, err := NewCommand("show-ref", "--verify", "--", name).RunInDir(repoPath) | ||||
|   | ||||
| @@ -6,6 +6,7 @@ package private | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| @@ -56,6 +57,7 @@ type HookOptions struct { | ||||
| 	GitPushOptions                  GitPushOptions | ||||
| 	PullRequestID                   int64 | ||||
| 	IsDeployKey                     bool | ||||
| 	IsWiki                          bool | ||||
| } | ||||
|  | ||||
| // SSHLogOption ssh log options | ||||
| @@ -79,6 +81,23 @@ type HookPostReceiveBranchResult struct { | ||||
| 	URL     string | ||||
| } | ||||
|  | ||||
| // HockProcReceiveResult represents an individual result from ProcReceive | ||||
| type HockProcReceiveResult struct { | ||||
| 	Results []HockProcReceiveRefResult | ||||
| 	Err     string | ||||
| } | ||||
|  | ||||
| // HockProcReceiveRefResult represents an individual result from ProcReceive | ||||
| type HockProcReceiveRefResult struct { | ||||
| 	OldOID       string | ||||
| 	NewOID       string | ||||
| 	Ref          string | ||||
| 	OriginalRef  string | ||||
| 	IsForcePush  bool | ||||
| 	IsNotMatched bool | ||||
| 	Err          string | ||||
| } | ||||
|  | ||||
| // HookPreReceive check whether the provided commits are allowed | ||||
| func HookPreReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (int, string) { | ||||
| 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/pre-receive/%s/%s", | ||||
| @@ -130,6 +149,33 @@ func HookPostReceive(ctx context.Context, ownerName, repoName string, opts HookO | ||||
| 	return res, "" | ||||
| } | ||||
|  | ||||
| // HookProcReceive proc-receive hook | ||||
| func HookProcReceive(ctx context.Context, ownerName, repoName string, opts HookOptions) (*HockProcReceiveResult, error) { | ||||
| 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/proc-receive/%s/%s", | ||||
| 		url.PathEscape(ownerName), | ||||
| 		url.PathEscape(repoName), | ||||
| 	) | ||||
|  | ||||
| 	req := newInternalRequest(ctx, reqURL, "POST") | ||||
| 	req = req.Header("Content-Type", "application/json") | ||||
| 	req.SetTimeout(60*time.Second, time.Duration(60+len(opts.OldCommitIDs))*time.Second) | ||||
| 	jsonBytes, _ := json.Marshal(opts) | ||||
| 	req.Body(jsonBytes) | ||||
| 	resp, err := req.Response() | ||||
| 	if err != nil { | ||||
| 		return nil, fmt.Errorf("Unable to contact gitea: %v", err.Error()) | ||||
| 	} | ||||
| 	defer resp.Body.Close() | ||||
|  | ||||
| 	if resp.StatusCode != http.StatusOK { | ||||
| 		return nil, errors.New(decodeJSONError(resp).Err) | ||||
| 	} | ||||
| 	res := &HockProcReceiveResult{} | ||||
| 	_ = json.NewDecoder(resp.Body).Decode(res) | ||||
|  | ||||
| 	return res, nil | ||||
| } | ||||
|  | ||||
| // SetDefaultBranch will set the default branch to the provided branch for the provided repository | ||||
| func SetDefaultBranch(ctx context.Context, ownerName, repoName, branch string) error { | ||||
| 	reqURL := setting.LocalURL + fmt.Sprintf("api/internal/hook/set-default-branch/%s/%s/%s", | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| @@ -75,6 +76,14 @@ done | ||||
| 		fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s update $1 $2 $3\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), | ||||
| 		fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s post-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf)), | ||||
| 	} | ||||
|  | ||||
| 	if git.SupportProcReceive { | ||||
| 		hookNames = append(hookNames, "proc-receive") | ||||
| 		hookTpls = append(hookTpls, | ||||
| 			fmt.Sprintf("#!/usr/bin/env %s\n%s hook --config=%s proc-receive\n", setting.ScriptType, util.ShellEscape(setting.AppPath), util.ShellEscape(setting.CustomConf))) | ||||
| 		giteaHookTpls = append(giteaHookTpls, "") | ||||
| 	} | ||||
|  | ||||
| 	return | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -310,7 +310,7 @@ func CreatePullRequest(ctx *context.APIContext) { | ||||
| 	defer headGitRepo.Close() | ||||
|  | ||||
| 	// Check if another PR exists with the same targets | ||||
| 	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch) | ||||
| 	existingPr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub) | ||||
| 	if err != nil { | ||||
| 		if !models.IsErrPullRequestNotExist(err) { | ||||
| 			ctx.Error(http.StatusInternalServerError, "GetUnmergedPullRequest", err) | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/services/agit" | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
| 	repo_service "code.gitea.io/gitea/services/repository" | ||||
| ) | ||||
| @@ -155,6 +156,56 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { | ||||
| 			private.GitQuarantinePath+"="+opts.GitQuarantinePath) | ||||
| 	} | ||||
|  | ||||
| 	if git.SupportProcReceive { | ||||
| 		pusher, err := models.GetUserByID(opts.UserID) | ||||
| 		if err != nil { | ||||
| 			log.Error("models.GetUserByID:%v", err) | ||||
| 			ctx.Error(http.StatusInternalServerError, "") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		perm, err := models.GetUserRepoPermission(repo, pusher) | ||||
| 		if err != nil { | ||||
| 			log.Error("models.GetUserRepoPermission:%v", err) | ||||
| 			ctx.Error(http.StatusInternalServerError, "") | ||||
| 			return | ||||
| 		} | ||||
|  | ||||
| 		canCreatePullRequest := perm.CanRead(models.UnitTypePullRequests) | ||||
|  | ||||
| 		for _, refFullName := range opts.RefFullNames { | ||||
| 			// if user want update other refs (branch or tag), | ||||
| 			// should check code write permission because | ||||
| 			// this check was delayed. | ||||
| 			if !strings.HasPrefix(refFullName, git.PullRequestPrefix) { | ||||
| 				if !perm.CanWrite(models.UnitTypeCode) { | ||||
| 					ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 						"err": "User permission denied.", | ||||
| 					}) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				break | ||||
| 			} else if repo.IsEmpty { | ||||
| 				ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 					"err": "Can't create pull request for an empty repository.", | ||||
| 				}) | ||||
| 				return | ||||
| 			} else if !canCreatePullRequest { | ||||
| 				ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 					"err": "User permission denied.", | ||||
| 				}) | ||||
| 				return | ||||
| 			} else if opts.IsWiki { | ||||
| 				// TODO: maybe can do it ... | ||||
| 				ctx.JSON(http.StatusForbidden, map[string]interface{}{ | ||||
| 					"err": "not support send pull request to wiki.", | ||||
| 				}) | ||||
| 				return | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	protectedTags, err := repo.GetProtectedTags() | ||||
| 	if err != nil { | ||||
| 		log.Error("Unable to get protected tags for %-v Error: %v", repo, err) | ||||
| @@ -392,11 +443,35 @@ func HookPreReceive(ctx *gitea_context.PrivateContext) { | ||||
| 				}) | ||||
| 				return | ||||
| 			} | ||||
| 		} else if git.SupportProcReceive && strings.HasPrefix(refFullName, git.PullRequestPrefix) { | ||||
| 			baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):] | ||||
|  | ||||
| 			baseBranchExist := false | ||||
| 			if gitRepo.IsBranchExist(baseBranchName) { | ||||
| 				baseBranchExist = true | ||||
| 			} | ||||
|  | ||||
| 			if !baseBranchExist { | ||||
| 				for p, v := range baseBranchName { | ||||
| 					if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { | ||||
| 						baseBranchExist = true | ||||
| 						break | ||||
| 					} | ||||
| 				} | ||||
| 			} | ||||
|  | ||||
| 			if !baseBranchExist { | ||||
| 				ctx.JSON(http.StatusForbidden, private.Response{ | ||||
| 					Err: fmt.Sprintf("Unexpected ref: %s", refFullName), | ||||
| 				}) | ||||
| 				return | ||||
| 			} | ||||
| 		} else { | ||||
| 			log.Error("Unexpected ref: %s", refFullName) | ||||
| 			ctx.JSON(http.StatusInternalServerError, private.Response{ | ||||
| 				Err: fmt.Sprintf("Unexpected ref: %s", refFullName), | ||||
| 			}) | ||||
| 			return | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| @@ -537,7 +612,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| 			pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch) | ||||
| 			pr, err := models.GetUnmergedPullRequest(repo.ID, baseRepo.ID, branch, baseRepo.DefaultBranch, models.PullRequestFlowGithub) | ||||
| 			if err != nil && !models.IsErrPullRequestNotExist(err) { | ||||
| 				log.Error("Failed to get active PR in: %-v Branch: %s to: %-v Branch: %s Error: %v", repo, branch, baseRepo, baseRepo.DefaultBranch, err) | ||||
| 				ctx.JSON(http.StatusInternalServerError, private.HookPostReceiveResult{ | ||||
| @@ -574,6 +649,30 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // HookProcReceive proc-receive hook | ||||
| func HookProcReceive(ctx *gitea_context.PrivateContext) { | ||||
| 	opts := web.GetForm(ctx).(*private.HookOptions) | ||||
| 	if !git.SupportProcReceive { | ||||
| 		ctx.Status(http.StatusNotFound) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	cancel := loadRepositoryAndGitRepoByParams(ctx) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
| 	defer cancel() | ||||
|  | ||||
| 	results := agit.ProcRecive(ctx, opts) | ||||
| 	if ctx.Written() { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.JSON(http.StatusOK, private.HockProcReceiveResult{ | ||||
| 		Results: results, | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| // SetDefaultBranch updates the default branch | ||||
| func SetDefaultBranch(ctx *gitea_context.PrivateContext) { | ||||
| 	ownerName := ctx.Params(":owner") | ||||
| @@ -618,3 +717,44 @@ func SetDefaultBranch(ctx *gitea_context.PrivateContext) { | ||||
| 	} | ||||
| 	ctx.PlainText(http.StatusOK, []byte("success")) | ||||
| } | ||||
|  | ||||
| func loadRepositoryAndGitRepoByParams(ctx *gitea_context.PrivateContext) context.CancelFunc { | ||||
| 	ownerName := ctx.Params(":owner") | ||||
| 	repoName := ctx.Params(":repo") | ||||
|  | ||||
| 	repo, err := models.GetRepositoryByOwnerAndName(ownerName, repoName) | ||||
| 	if err != nil { | ||||
| 		log.Error("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err) | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"Err": fmt.Sprintf("Failed to get repository: %s/%s Error: %v", ownerName, repoName, err), | ||||
| 		}) | ||||
| 		return nil | ||||
| 	} | ||||
| 	if repo.OwnerName == "" { | ||||
| 		repo.OwnerName = ownerName | ||||
| 	} | ||||
|  | ||||
| 	gitRepo, err := git.OpenRepository(repo.RepoPath()) | ||||
| 	if err != nil { | ||||
| 		log.Error("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err) | ||||
| 		ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 			"Err": fmt.Sprintf("Failed to open repository: %s/%s Error: %v", ownerName, repoName, err), | ||||
| 		}) | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	ctx.Repo = &gitea_context.Repository{ | ||||
| 		Repository: repo, | ||||
| 		GitRepo:    gitRepo, | ||||
| 	} | ||||
|  | ||||
| 	// We opened it, we should close it | ||||
| 	cancel := func() { | ||||
| 		// If it's been set to nil then assume someone else has closed it. | ||||
| 		if ctx.Repo.GitRepo != nil { | ||||
| 			ctx.Repo.GitRepo.Close() | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return cancel | ||||
| } | ||||
|   | ||||
| @@ -58,6 +58,7 @@ func Routes() *web.Route { | ||||
| 	r.Post("/ssh/log", bind(private.SSHLogOption{}), SSHLog) | ||||
| 	r.Post("/hook/pre-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPreReceive) | ||||
| 	r.Post("/hook/post-receive/{owner}/{repo}", bind(private.HookOptions{}), HookPostReceive) | ||||
| 	r.Post("/hook/proc-receive/{owner}/{repo}", bind(private.HookOptions{}), HookProcReceive) | ||||
| 	r.Post("/hook/set-default-branch/{owner}/{repo}/{branch}", SetDefaultBranch) | ||||
| 	r.Get("/serv/none/{keyid}", ServNoCommand) | ||||
| 	r.Get("/serv/command/{keyid}/{owner}/{repo}", ServCommand) | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/private" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
| @@ -288,6 +289,11 @@ func ServCommand(ctx *context.PrivateContext) { | ||||
| 				return | ||||
| 			} | ||||
| 		} else { | ||||
| 			// Because of special ref "refs/for" .. , need delay write permission check | ||||
| 			if git.SupportProcReceive && unitType == models.UnitTypeCode { | ||||
| 				mode = models.AccessModeRead | ||||
| 			} | ||||
|  | ||||
| 			perm, err := models.GetUserRepoPermission(repo, user) | ||||
| 			if err != nil { | ||||
| 				log.Error("Unable to get permissions for %-v with key %d in %-v Error: %v", user, key.ID, repo, err) | ||||
|   | ||||
| @@ -653,7 +653,7 @@ func CompareDiff(ctx *context.Context) { | ||||
| 	ctx.Data["HeadTags"] = headTags | ||||
|  | ||||
| 	if ctx.Data["PageIsComparePull"] == true { | ||||
| 		pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch) | ||||
| 		pr, err := models.GetUnmergedPullRequest(headRepo.ID, ctx.Repo.Repository.ID, headBranch, baseBranch, models.PullRequestFlowGithub) | ||||
| 		if err != nil { | ||||
| 			if !models.IsErrPullRequestNotExist(err) { | ||||
| 				ctx.ServerError("GetUnmergedPullRequest", err) | ||||
|   | ||||
| @@ -198,6 +198,11 @@ func httpBase(ctx *context.Context) (h *serviceHandler) { | ||||
| 				return | ||||
| 			} | ||||
|  | ||||
| 			// Because of special ref "refs/for" .. , need delay write permission check | ||||
| 			if git.SupportProcReceive { | ||||
| 				accessMode = models.AccessModeRead | ||||
| 			} | ||||
|  | ||||
| 			if !perm.CanAccess(accessMode, unitType) { | ||||
| 				ctx.HandleText(http.StatusForbidden, "User permission denied") | ||||
| 				return | ||||
|   | ||||
| @@ -2047,7 +2047,7 @@ func NewComment(ctx *context.Context) { | ||||
| 			if form.Status == "reopen" && issue.IsPull { | ||||
| 				pull := issue.PullRequest | ||||
| 				var err error | ||||
| 				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch) | ||||
| 				pr, err = models.GetUnmergedPullRequest(pull.HeadRepoID, pull.BaseRepoID, pull.HeadBranch, pull.BaseBranch, pull.Flow) | ||||
| 				if err != nil { | ||||
| 					if !models.IsErrPullRequestNotExist(err) { | ||||
| 						ctx.ServerError("GetUnmergedPullRequest", err) | ||||
| @@ -2057,6 +2057,7 @@ func NewComment(ctx *context.Context) { | ||||
|  | ||||
| 				// Regenerate patch and test conflict. | ||||
| 				if pr == nil { | ||||
| 					issue.PullRequest.HeadCommitID = "" | ||||
| 					pull_service.AddToTaskQueue(issue.PullRequest) | ||||
| 				} | ||||
| 			} | ||||
|   | ||||
| @@ -427,10 +427,18 @@ func PrepareViewPullInfo(ctx *context.Context, issue *models.Issue) *git.Compare | ||||
| 		} | ||||
| 		defer headGitRepo.Close() | ||||
|  | ||||
| 		headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) | ||||
| 		if pull.Flow == models.PullRequestFlowGithub { | ||||
| 			headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch) | ||||
| 		} else { | ||||
| 			headBranchExist = git.IsReferenceExist(baseGitRepo.Path, pull.GetGitRefName()) | ||||
| 		} | ||||
|  | ||||
| 		if headBranchExist { | ||||
| 			headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) | ||||
| 			if pull.Flow != models.PullRequestFlowGithub { | ||||
| 				headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName()) | ||||
| 			} else { | ||||
| 				headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch) | ||||
| 			} | ||||
| 			if err != nil { | ||||
| 				ctx.ServerError("GetBranchCommitID", err) | ||||
| 				return nil | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/util" | ||||
| 	"code.gitea.io/gitea/modules/web" | ||||
| 	"code.gitea.io/gitea/modules/web/middleware" | ||||
| 	"code.gitea.io/gitea/services/agit" | ||||
| 	"code.gitea.io/gitea/services/forms" | ||||
|  | ||||
| 	"github.com/unknwon/i18n" | ||||
| @@ -76,6 +77,14 @@ func HandleUsernameChange(ctx *context.Context, user *models.User, newName strin | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// update all agit flow pull request header | ||||
| 	err := agit.UserNameChanged(user, newName) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("agit.UserNameChanged", err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	log.Trace("User name changed: %s -> %s", user.Name, newName) | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import ( | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/httpcache" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/metrics" | ||||
| @@ -146,6 +147,21 @@ func Routes() *web.Route { | ||||
| 		routes.Get("/metrics", append(common, Metrics)...) | ||||
| 	} | ||||
|  | ||||
| 	routes.Get("/ssh_info", func(rw http.ResponseWriter, req *http.Request) { | ||||
| 		if !git.SupportProcReceive { | ||||
| 			rw.WriteHeader(404) | ||||
| 			return | ||||
| 		} | ||||
| 		rw.Header().Set("content-type", "text/json;charset=UTF-8") | ||||
| 		_, err := rw.Write([]byte(`{"type":"gitea","version":1}`)) | ||||
| 		if err != nil { | ||||
| 			log.Error("fail to write result: err: %v", err) | ||||
| 			rw.WriteHeader(500) | ||||
| 			return | ||||
| 		} | ||||
| 		rw.WriteHeader(200) | ||||
| 	}) | ||||
|  | ||||
| 	// Removed: toolbox.Toolboxer middleware will provide debug information which seems unnecessary | ||||
| 	common = append(common, context.Contexter()) | ||||
|  | ||||
|   | ||||
							
								
								
									
										288
									
								
								services/agit/agit.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										288
									
								
								services/agit/agit.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,288 @@ | ||||
| // Copyright 2021 The Gitea Authors. All rights reserved. | ||||
| // Use of this source code is governed by a MIT-style | ||||
| // license that can be found in the LICENSE file. | ||||
|  | ||||
| package agit | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"net/http" | ||||
| 	"os" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/models" | ||||
| 	"code.gitea.io/gitea/modules/context" | ||||
| 	"code.gitea.io/gitea/modules/git" | ||||
| 	"code.gitea.io/gitea/modules/log" | ||||
| 	"code.gitea.io/gitea/modules/notification" | ||||
| 	"code.gitea.io/gitea/modules/private" | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
| ) | ||||
|  | ||||
| // ProcRecive handle proc receive work | ||||
| func ProcRecive(ctx *context.PrivateContext, opts *private.HookOptions) []private.HockProcReceiveRefResult { | ||||
| 	// TODO: Add more options? | ||||
| 	var ( | ||||
| 		topicBranch string | ||||
| 		title       string | ||||
| 		description string | ||||
| 		forcePush   bool | ||||
| 	) | ||||
|  | ||||
| 	results := make([]private.HockProcReceiveRefResult, 0, len(opts.OldCommitIDs)) | ||||
| 	repo := ctx.Repo.Repository | ||||
| 	gitRepo := ctx.Repo.GitRepo | ||||
| 	ownerName := ctx.Repo.Repository.OwnerName | ||||
| 	repoName := ctx.Repo.Repository.Name | ||||
|  | ||||
| 	topicBranch = opts.GitPushOptions["topic"] | ||||
| 	_, forcePush = opts.GitPushOptions["force-push"] | ||||
|  | ||||
| 	for i := range opts.OldCommitIDs { | ||||
| 		if opts.NewCommitIDs[i] == git.EmptySHA { | ||||
| 			results = append(results, private.HockProcReceiveRefResult{ | ||||
| 				OriginalRef: opts.RefFullNames[i], | ||||
| 				OldOID:      opts.OldCommitIDs[i], | ||||
| 				NewOID:      opts.NewCommitIDs[i], | ||||
| 				Err:         "Can't delete not exist branch", | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if !strings.HasPrefix(opts.RefFullNames[i], git.PullRequestPrefix) { | ||||
| 			results = append(results, private.HockProcReceiveRefResult{ | ||||
| 				IsNotMatched: true, | ||||
| 				OriginalRef:  opts.RefFullNames[i], | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		baseBranchName := opts.RefFullNames[i][len(git.PullRequestPrefix):] | ||||
| 		curentTopicBranch := "" | ||||
| 		if !gitRepo.IsBranchExist(baseBranchName) { | ||||
| 			// try match refs/for/<target-branch>/<topic-branch> | ||||
| 			for p, v := range baseBranchName { | ||||
| 				if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { | ||||
| 					curentTopicBranch = baseBranchName[p+1:] | ||||
| 					baseBranchName = baseBranchName[:p] | ||||
| 					break | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		if len(topicBranch) == 0 && len(curentTopicBranch) == 0 { | ||||
| 			results = append(results, private.HockProcReceiveRefResult{ | ||||
| 				OriginalRef: opts.RefFullNames[i], | ||||
| 				OldOID:      opts.OldCommitIDs[i], | ||||
| 				NewOID:      opts.NewCommitIDs[i], | ||||
| 				Err:         "topic-branch is not set", | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		headBranch := "" | ||||
| 		userName := strings.ToLower(opts.UserName) | ||||
|  | ||||
| 		if len(curentTopicBranch) == 0 { | ||||
| 			curentTopicBranch = topicBranch | ||||
| 		} | ||||
|  | ||||
| 		// because different user maybe want to use same topic, | ||||
| 		// So it's better to make sure the topic branch name | ||||
| 		// has user name prefix | ||||
| 		if !strings.HasPrefix(curentTopicBranch, userName+"/") { | ||||
| 			headBranch = userName + "/" + curentTopicBranch | ||||
| 		} else { | ||||
| 			headBranch = curentTopicBranch | ||||
| 		} | ||||
|  | ||||
| 		pr, err := models.GetUnmergedPullRequest(repo.ID, repo.ID, headBranch, baseBranchName, models.PullRequestFlowAGit) | ||||
| 		if err != nil { | ||||
| 			if !models.IsErrPullRequestNotExist(err) { | ||||
| 				log.Error("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err) | ||||
| 				ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 					"Err": fmt.Sprintf("Failed to get unmerged agit flow pull request in repository: %s/%s Error: %v", ownerName, repoName, err), | ||||
| 				}) | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			// create a new pull request | ||||
| 			if len(title) == 0 { | ||||
| 				has := false | ||||
| 				title, has = opts.GitPushOptions["title"] | ||||
| 				if !has || len(title) == 0 { | ||||
| 					commit, err := gitRepo.GetCommit(opts.NewCommitIDs[i]) | ||||
| 					if err != nil { | ||||
| 						log.Error("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err) | ||||
| 						ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 							"Err": fmt.Sprintf("Failed to get commit %s in repository: %s/%s Error: %v", opts.NewCommitIDs[i], ownerName, repoName, err), | ||||
| 						}) | ||||
| 						return nil | ||||
| 					} | ||||
| 					title = strings.Split(commit.CommitMessage, "\n")[0] | ||||
| 				} | ||||
| 				description = opts.GitPushOptions["description"] | ||||
| 			} | ||||
|  | ||||
| 			pusher, err := models.GetUserByID(opts.UserID) | ||||
| 			if err != nil { | ||||
| 				log.Error("Failed to get user. Error: %v", err) | ||||
| 				ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 					"Err": fmt.Sprintf("Failed to get user. Error: %v", err), | ||||
| 				}) | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			prIssue := &models.Issue{ | ||||
| 				RepoID:   repo.ID, | ||||
| 				Title:    title, | ||||
| 				PosterID: pusher.ID, | ||||
| 				Poster:   pusher, | ||||
| 				IsPull:   true, | ||||
| 				Content:  description, | ||||
| 			} | ||||
|  | ||||
| 			pr := &models.PullRequest{ | ||||
| 				HeadRepoID:   repo.ID, | ||||
| 				BaseRepoID:   repo.ID, | ||||
| 				HeadBranch:   headBranch, | ||||
| 				HeadCommitID: opts.NewCommitIDs[i], | ||||
| 				BaseBranch:   baseBranchName, | ||||
| 				HeadRepo:     repo, | ||||
| 				BaseRepo:     repo, | ||||
| 				MergeBase:    "", | ||||
| 				Type:         models.PullRequestGitea, | ||||
| 				Flow:         models.PullRequestFlowAGit, | ||||
| 			} | ||||
|  | ||||
| 			if err := pull_service.NewPullRequest(repo, prIssue, []int64{}, []string{}, pr, []int64{}); err != nil { | ||||
| 				if models.IsErrUserDoesNotHaveAccessToRepo(err) { | ||||
| 					ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error()) | ||||
| 					return nil | ||||
| 				} | ||||
| 				ctx.Error(http.StatusInternalServerError, "NewPullRequest", err.Error()) | ||||
| 				return nil | ||||
| 			} | ||||
|  | ||||
| 			log.Trace("Pull request created: %d/%d", repo.ID, prIssue.ID) | ||||
|  | ||||
| 			results = append(results, private.HockProcReceiveRefResult{ | ||||
| 				Ref:         pr.GetGitRefName(), | ||||
| 				OriginalRef: opts.RefFullNames[i], | ||||
| 				OldOID:      git.EmptySHA, | ||||
| 				NewOID:      opts.NewCommitIDs[i], | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		// update exist pull request | ||||
| 		if err := pr.LoadBaseRepo(); err != nil { | ||||
| 			log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"Err": fmt.Sprintf("Unable to load base repository for PR[%d] Error: %v", pr.ID, err), | ||||
| 			}) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		oldCommitID, err := gitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 		if err != nil { | ||||
| 			log.Error("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"Err": fmt.Sprintf("Unable to get ref commit id in base repository for PR[%d] Error: %v", pr.ID, err), | ||||
| 			}) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		if oldCommitID == opts.NewCommitIDs[i] { | ||||
| 			results = append(results, private.HockProcReceiveRefResult{ | ||||
| 				OriginalRef: opts.RefFullNames[i], | ||||
| 				OldOID:      opts.OldCommitIDs[i], | ||||
| 				NewOID:      opts.NewCommitIDs[i], | ||||
| 				Err:         "new commit is same with old commit", | ||||
| 			}) | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		if !forcePush { | ||||
| 			output, err := git.NewCommand("rev-list", "--max-count=1", oldCommitID, "^"+opts.NewCommitIDs[i]).RunInDirWithEnv(repo.RepoPath(), os.Environ()) | ||||
| 			if err != nil { | ||||
| 				log.Error("Unable to detect force push between: %s and %s in %-v Error: %v", oldCommitID, opts.NewCommitIDs[i], repo, err) | ||||
| 				ctx.JSON(http.StatusInternalServerError, private.Response{ | ||||
| 					Err: fmt.Sprintf("Fail to detect force push: %v", err), | ||||
| 				}) | ||||
| 				return nil | ||||
| 			} else if len(output) > 0 { | ||||
| 				results = append(results, private.HockProcReceiveRefResult{ | ||||
| 					OriginalRef: oldCommitID, | ||||
| 					OldOID:      opts.OldCommitIDs[i], | ||||
| 					NewOID:      opts.NewCommitIDs[i], | ||||
| 					Err:         "request `force-push` push option", | ||||
| 				}) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		pr.HeadCommitID = opts.NewCommitIDs[i] | ||||
| 		if err = pull_service.UpdateRef(pr); err != nil { | ||||
| 			log.Error("Failed to update pull ref. Error: %v", err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"Err": fmt.Sprintf("Failed to update pull ref. Error: %v", err), | ||||
| 			}) | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		pull_service.AddToTaskQueue(pr) | ||||
| 		pusher, err := models.GetUserByID(opts.UserID) | ||||
| 		if err != nil { | ||||
| 			log.Error("Failed to get user. Error: %v", err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"Err": fmt.Sprintf("Failed to get user. Error: %v", err), | ||||
| 			}) | ||||
| 			return nil | ||||
| 		} | ||||
| 		err = pr.LoadIssue() | ||||
| 		if err != nil { | ||||
| 			log.Error("Failed to load pull issue. Error: %v", err) | ||||
| 			ctx.JSON(http.StatusInternalServerError, map[string]interface{}{ | ||||
| 				"Err": fmt.Sprintf("Failed to load pull issue. Error: %v", err), | ||||
| 			}) | ||||
| 			return nil | ||||
| 		} | ||||
| 		comment, err := models.CreatePushPullComment(pusher, pr, oldCommitID, opts.NewCommitIDs[i]) | ||||
| 		if err == nil && comment != nil { | ||||
| 			notification.NotifyPullRequestPushCommits(pusher, pr, comment) | ||||
| 		} | ||||
| 		notification.NotifyPullRequestSynchronized(pusher, pr) | ||||
| 		isForcePush := comment != nil && comment.IsForcePush | ||||
|  | ||||
| 		results = append(results, private.HockProcReceiveRefResult{ | ||||
| 			OldOID:      oldCommitID, | ||||
| 			NewOID:      opts.NewCommitIDs[i], | ||||
| 			Ref:         pr.GetGitRefName(), | ||||
| 			OriginalRef: opts.RefFullNames[i], | ||||
| 			IsForcePush: isForcePush, | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	return results | ||||
| } | ||||
|  | ||||
| // UserNameChanged hanle user name change for agit flow pull | ||||
| func UserNameChanged(user *models.User, newName string) error { | ||||
| 	pulls, err := models.GetAllUnmergedAgitPullRequestByPoster(user.ID) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	newName = strings.ToLower(newName) | ||||
|  | ||||
| 	for _, pull := range pulls { | ||||
| 		pull.HeadBranch = strings.TrimPrefix(pull.HeadBranch, user.LowerName+"/") | ||||
| 		pull.HeadBranch = newName + "/" + pull.HeadBranch | ||||
| 		if err = pull.UpdateCols("head_branch"); err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
| @@ -108,13 +108,21 @@ func GetPullRequestCommitStatusState(pr *models.PullRequest) (structs.CommitStat | ||||
| 	} | ||||
| 	defer headGitRepo.Close() | ||||
|  | ||||
| 	if !headGitRepo.IsBranchExist(pr.HeadBranch) { | ||||
| 	if pr.Flow == models.PullRequestFlowGithub && !headGitRepo.IsBranchExist(pr.HeadBranch) { | ||||
| 		return "", errors.New("Head branch does not exist, can not merge") | ||||
| 	} | ||||
| 	if pr.Flow == models.PullRequestFlowAGit && !git.IsReferenceExist(headGitRepo.Path, pr.GetGitRefName()) { | ||||
| 		return "", errors.New("Head branch does not exist, can not merge") | ||||
| 	} | ||||
|  | ||||
| 	sha, err := headGitRepo.GetBranchCommitID(pr.HeadBranch) | ||||
| 	var sha string | ||||
| 	if pr.Flow == models.PullRequestFlowGithub { | ||||
| 		sha, err = headGitRepo.GetBranchCommitID(pr.HeadBranch) | ||||
| 	} else { | ||||
| 		sha, err = headGitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return "", errors.Wrap(err, "GetBranchCommitID") | ||||
| 		return "", err | ||||
| 	} | ||||
|  | ||||
| 	if err := pr.LoadBaseRepo(); err != nil { | ||||
|   | ||||
| @@ -49,7 +49,12 @@ func NewPullRequest(repo *models.Repository, pull *models.Issue, labelIDs []int6 | ||||
| 	pr.Issue = pull | ||||
| 	pull.PullRequest = pr | ||||
|  | ||||
| 	if err := PushToBaseRepo(pr); err != nil { | ||||
| 	if pr.Flow == models.PullRequestFlowGithub { | ||||
| 		err = PushToBaseRepo(pr) | ||||
| 	} else { | ||||
| 		err = UpdateRef(pr) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| @@ -145,7 +150,7 @@ func ChangeTargetBranch(pr *models.PullRequest, doer *models.User, targetBranch | ||||
| 	} | ||||
|  | ||||
| 	// Check if pull request for the new target branch already exists | ||||
| 	existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch) | ||||
| 	existingPr, err := models.GetUnmergedPullRequest(pr.HeadRepoID, pr.BaseRepoID, pr.HeadBranch, targetBranch, models.PullRequestFlowGithub) | ||||
| 	if existingPr != nil { | ||||
| 		return models.ErrPullRequestAlreadyExists{ | ||||
| 			ID:         existingPr.ID, | ||||
| @@ -281,8 +286,12 @@ func AddTestPullRequestTask(doer *models.User, repoID int64, branch string, isSy | ||||
|  | ||||
| 		for _, pr := range prs { | ||||
| 			log.Trace("Updating PR[%d]: composing new test task", pr.ID) | ||||
| 			if err := PushToBaseRepo(pr); err != nil { | ||||
| 				log.Error("PushToBaseRepo: %v", err) | ||||
| 			if pr.Flow == models.PullRequestFlowGithub { | ||||
| 				if err := PushToBaseRepo(pr); err != nil { | ||||
| 					log.Error("PushToBaseRepo: %v", err) | ||||
| 					continue | ||||
| 				} | ||||
| 			} else { | ||||
| 				continue | ||||
| 			} | ||||
|  | ||||
| @@ -451,6 +460,22 @@ func pushToBaseRepoHelper(pr *models.PullRequest, prefixHeadBranch string) (err | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // UpdateRef update refs/pull/id/head directly for agit flow pull request | ||||
| func UpdateRef(pr *models.PullRequest) (err error) { | ||||
| 	log.Trace("UpdateRef[%d]: upgate pull request ref in base repo '%s'", pr.ID, pr.GetGitRefName()) | ||||
| 	if err := pr.LoadBaseRepo(); err != nil { | ||||
| 		log.Error("Unable to load base repository for PR[%d] Error: %v", pr.ID, err) | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	_, err = git.NewCommand("update-ref", pr.GetGitRefName(), pr.HeadCommitID).RunInDir(pr.BaseRepo.RepoPath()) | ||||
| 	if err != nil { | ||||
| 		log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err) | ||||
| 	} | ||||
|  | ||||
| 	return err | ||||
| } | ||||
|  | ||||
| type errlist []error | ||||
|  | ||||
| func (errs errlist) Error() string { | ||||
| @@ -562,7 +587,17 @@ func GetSquashMergeCommitMessages(pr *models.PullRequest) string { | ||||
| 	} | ||||
| 	defer gitRepo.Close() | ||||
|  | ||||
| 	headCommit, err := gitRepo.GetBranchCommit(pr.HeadBranch) | ||||
| 	var headCommit *git.Commit | ||||
| 	if pr.Flow == models.PullRequestFlowGithub { | ||||
| 		headCommit, err = gitRepo.GetBranchCommit(pr.HeadBranch) | ||||
| 	} else { | ||||
| 		pr.HeadCommitID, err = gitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 		if err != nil { | ||||
| 			log.Error("Unable to get head commit: %s Error: %v", pr.GetGitRefName(), err) | ||||
| 			return "" | ||||
| 		} | ||||
| 		headCommit, err = gitRepo.GetCommit(pr.HeadCommitID) | ||||
| 	} | ||||
| 	if err != nil { | ||||
| 		log.Error("Unable to get head commit: %s Error: %v", pr.HeadBranch, err) | ||||
| 		return "" | ||||
| @@ -781,9 +816,20 @@ func IsHeadEqualWithBranch(pr *models.PullRequest, branchName string) (bool, err | ||||
| 	} | ||||
| 	defer headGitRepo.Close() | ||||
|  | ||||
| 	headCommit, err := headGitRepo.GetBranchCommit(pr.HeadBranch) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	var headCommit *git.Commit | ||||
| 	if pr.Flow == models.PullRequestFlowGithub { | ||||
| 		headCommit, err = headGitRepo.GetBranchCommit(pr.HeadBranch) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} else { | ||||
| 		pr.HeadCommitID, err = baseGitRepo.GetRefCommitID(pr.GetGitRefName()) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 		if headCommit, err = baseGitRepo.GetCommit(pr.HeadCommitID); err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| 	} | ||||
| 	return baseCommit.HasPreviousCommit(headCommit.ID) | ||||
| } | ||||
|   | ||||
| @@ -140,7 +140,15 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) { | ||||
|  | ||||
| 	trackingBranch := "tracking" | ||||
| 	// Fetch head branch | ||||
| 	if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, git.BranchPrefix+pr.HeadBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { | ||||
| 	var headBranch string | ||||
| 	if pr.Flow == models.PullRequestFlowGithub { | ||||
| 		headBranch = git.BranchPrefix + pr.HeadBranch | ||||
| 	} else if len(pr.HeadCommitID) == 40 { // for not created pull request | ||||
| 		headBranch = pr.HeadCommitID | ||||
| 	} else { | ||||
| 		headBranch = pr.GetGitRefName() | ||||
| 	} | ||||
| 	if err := git.NewCommand("fetch", "--no-tags", remoteRepoName, headBranch+":"+trackingBranch).RunInDirPipeline(tmpBasePath, &outbuf, &errbuf); err != nil { | ||||
| 		if err := models.RemoveTemporaryPath(tmpBasePath); err != nil { | ||||
| 			log.Error("CreateTempRepo: RemoveTemporaryPath: %s", err) | ||||
| 		} | ||||
| @@ -150,7 +158,7 @@ func createTemporaryRepo(pr *models.PullRequest) (string, error) { | ||||
| 			} | ||||
| 		} | ||||
| 		log.Error("Unable to fetch head_repo head branch [%s:%s -> tracking in %s]: %v:\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, tmpBasePath, err, outbuf.String(), errbuf.String()) | ||||
| 		return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), pr.HeadBranch, err, outbuf.String(), errbuf.String()) | ||||
| 		return "", fmt.Errorf("Unable to fetch head_repo head branch [%s:%s -> tracking in tmpBasePath]: %v\n%s\n%s", pr.HeadRepo.FullName(), headBranch, err, outbuf.String(), errbuf.String()) | ||||
| 	} | ||||
| 	outbuf.Reset() | ||||
| 	errbuf.Reset() | ||||
|   | ||||
| @@ -22,6 +22,11 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error { | ||||
| 		BaseBranch: pull.HeadBranch, | ||||
| 	} | ||||
|  | ||||
| 	if pull.Flow == models.PullRequestFlowAGit { | ||||
| 		// TODO: Not support update agit flow pull request's head branch | ||||
| 		return fmt.Errorf("Not support update agit flow pull request's head branch") | ||||
| 	} | ||||
|  | ||||
| 	if err := pr.LoadHeadRepo(); err != nil { | ||||
| 		log.Error("LoadHeadRepo: %v", err) | ||||
| 		return fmt.Errorf("LoadHeadRepo: %v", err) | ||||
| @@ -48,6 +53,10 @@ func Update(pull *models.PullRequest, doer *models.User, message string) error { | ||||
|  | ||||
| // IsUserAllowedToUpdate check if user is allowed to update PR with given permissions and branch protections | ||||
| func IsUserAllowedToUpdate(pull *models.PullRequest, user *models.User) (bool, error) { | ||||
| 	if pull.Flow == models.PullRequestFlowAGit { | ||||
| 		return false, nil | ||||
| 	} | ||||
|  | ||||
| 	if user == nil { | ||||
| 		return false, nil | ||||
| 	} | ||||
|   | ||||
| @@ -461,13 +461,17 @@ | ||||
| 									{{end}} | ||||
| 								</div> | ||||
| 							</div> | ||||
| 							<div class="instruct-toggle ml-3">{{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}}</div> | ||||
| 							<div class="instruct-toggle ml-3"> {{$.i18n.Tr "repo.pulls.merge_instruction_hint" | Safe}} </div> | ||||
| 							<div class="instruct-content" style="display:none"> | ||||
| 								<div class="ui divider"></div> | ||||
| 								<div><h3 class="di">{{$.i18n.Tr "step1"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step1_desc"}}</div> | ||||
| 								<div class="ui secondary segment"> | ||||
| 									<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div> | ||||
| 									<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div> | ||||
| 									{{if eq .Issue.PullRequest.Flow 0}} | ||||
| 										<div>git checkout -b {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.OwnerName}}-{{end}}{{.Issue.PullRequest.HeadBranch}} {{.Issue.PullRequest.BaseBranch}}</div> | ||||
| 										<div>git pull {{if ne .Issue.PullRequest.HeadRepo.ID .Issue.PullRequest.BaseRepo.ID}}{{.Issue.PullRequest.HeadRepo.HTMLURL}}{{else}}origin{{end}} {{.Issue.PullRequest.HeadBranch}}</div> | ||||
| 									{{else}} | ||||
| 										<div>git fetch origin {{.Issue.PullRequest.GetGitRefName}}:{{.Issue.PullRequest.HeadBranch}}</div> | ||||
| 									{{end}} | ||||
| 								</div> | ||||
| 								<div><h3 class="di">{{$.i18n.Tr "step2"}} </h3>{{$.i18n.Tr "repo.pulls.merge_instruction_step2_desc"}}</div> | ||||
| 								<div class="ui secondary segment"> | ||||
|   | ||||
		Reference in New Issue
	
	Block a user