mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-26 12:27:06 +00:00 
			
		
		
		
	Make git push options accept short name (#32245)
Just like what most CLI parsers do: `--opt` means `opt=true` Then users could use `-o force-push` as `-o force-push=true`
This commit is contained in:
		| @@ -593,6 +593,7 @@ Gitea or set your environment appropriately.`, "") | |||||||
| 	hookOptions := private.HookOptions{ | 	hookOptions := private.HookOptions{ | ||||||
| 		UserName:       pusherName, | 		UserName:       pusherName, | ||||||
| 		UserID:         pusherID, | 		UserID:         pusherID, | ||||||
|  | 		GitPushOptions: make(map[string]string), | ||||||
| 	} | 	} | ||||||
| 	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) | 	hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize) | ||||||
| 	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) | 	hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize) | ||||||
| @@ -617,8 +618,6 @@ Gitea or set your environment appropriately.`, "") | |||||||
| 		hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2])) | 		hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2])) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	hookOptions.GitPushOptions = make(map[string]string) |  | ||||||
|  |  | ||||||
| 	if hasPushOptions { | 	if hasPushOptions { | ||||||
| 		for { | 		for { | ||||||
| 			rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) | 			rs, err = readPktLine(ctx, reader, pktLineTypeUnknow) | ||||||
| @@ -629,11 +628,7 @@ Gitea or set your environment appropriately.`, "") | |||||||
| 			if rs.Type == pktLineTypeFlush { | 			if rs.Type == pktLineTypeFlush { | ||||||
| 				break | 				break | ||||||
| 			} | 			} | ||||||
|  | 			hookOptions.GitPushOptions.AddFromKeyValue(string(rs.Data)) | ||||||
| 			kv := strings.SplitN(string(rs.Data), "=", 2) |  | ||||||
| 			if len(kv) == 2 { |  | ||||||
| 				hookOptions.GitPushOptions[kv[0]] = kv[1] |  | ||||||
| 			} |  | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,11 +7,9 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"strconv" |  | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"code.gitea.io/gitea/modules/git" | 	"code.gitea.io/gitea/modules/git" | ||||||
| 	"code.gitea.io/gitea/modules/optional" |  | ||||||
| 	"code.gitea.io/gitea/modules/repository" | 	"code.gitea.io/gitea/modules/repository" | ||||||
| 	"code.gitea.io/gitea/modules/setting" | 	"code.gitea.io/gitea/modules/setting" | ||||||
| ) | ) | ||||||
| @@ -24,25 +22,6 @@ const ( | |||||||
| 	GitPushOptionCount              = "GIT_PUSH_OPTION_COUNT" | 	GitPushOptionCount              = "GIT_PUSH_OPTION_COUNT" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // GitPushOptions is a wrapper around a map[string]string |  | ||||||
| type GitPushOptions map[string]string |  | ||||||
|  |  | ||||||
| // GitPushOptions keys |  | ||||||
| const ( |  | ||||||
| 	GitPushOptionRepoPrivate  = "repo.private" |  | ||||||
| 	GitPushOptionRepoTemplate = "repo.template" |  | ||||||
| ) |  | ||||||
|  |  | ||||||
| // Bool checks for a key in the map and parses as a boolean |  | ||||||
| func (g GitPushOptions) Bool(key string) optional.Option[bool] { |  | ||||||
| 	if val, ok := g[key]; ok { |  | ||||||
| 		if b, err := strconv.ParseBool(val); err == nil { |  | ||||||
| 			return optional.Some(b) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 	return optional.None[bool]() |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // HookOptions represents the options for the Hook calls | // HookOptions represents the options for the Hook calls | ||||||
| type HookOptions struct { | type HookOptions struct { | ||||||
| 	OldCommitIDs                    []string | 	OldCommitIDs                    []string | ||||||
|   | |||||||
							
								
								
									
										45
									
								
								modules/private/pushoptions.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								modules/private/pushoptions.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,45 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package private | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"strconv" | ||||||
|  | 	"strings" | ||||||
|  |  | ||||||
|  | 	"code.gitea.io/gitea/modules/optional" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GitPushOptions is a wrapper around a map[string]string | ||||||
|  | type GitPushOptions map[string]string | ||||||
|  |  | ||||||
|  | // GitPushOptions keys | ||||||
|  | const ( | ||||||
|  | 	GitPushOptionRepoPrivate  = "repo.private" | ||||||
|  | 	GitPushOptionRepoTemplate = "repo.template" | ||||||
|  | 	GitPushOptionForcePush    = "force-push" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // Bool checks for a key in the map and parses as a boolean | ||||||
|  | // An option without value is considered true, eg: "-o force-push" or "-o repo.private" | ||||||
|  | func (g GitPushOptions) Bool(key string) optional.Option[bool] { | ||||||
|  | 	if val, ok := g[key]; ok { | ||||||
|  | 		if val == "" { | ||||||
|  | 			return optional.Some(true) | ||||||
|  | 		} | ||||||
|  | 		if b, err := strconv.ParseBool(val); err == nil { | ||||||
|  | 			return optional.Some(b) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return optional.None[bool]() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // AddFromKeyValue adds a key value pair to the map by "key=value" format or "key" for empty value | ||||||
|  | func (g GitPushOptions) AddFromKeyValue(line string) { | ||||||
|  | 	kv := strings.SplitN(line, "=", 2) | ||||||
|  | 	if len(kv) == 2 { | ||||||
|  | 		g[kv[0]] = kv[1] | ||||||
|  | 	} else { | ||||||
|  | 		g[kv[0]] = "" | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										30
									
								
								modules/private/pushoptions_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								modules/private/pushoptions_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | // Copyright 2024 The Gitea Authors. All rights reserved. | ||||||
|  | // SPDX-License-Identifier: MIT | ||||||
|  |  | ||||||
|  | package private | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/assert" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func TestGitPushOptions(t *testing.T) { | ||||||
|  | 	o := GitPushOptions{} | ||||||
|  |  | ||||||
|  | 	v := o.Bool("no-such") | ||||||
|  | 	assert.False(t, v.Has()) | ||||||
|  | 	assert.False(t, v.Value()) | ||||||
|  |  | ||||||
|  | 	o.AddFromKeyValue("opt1=a=b") | ||||||
|  | 	o.AddFromKeyValue("opt2=false") | ||||||
|  | 	o.AddFromKeyValue("opt3=true") | ||||||
|  | 	o.AddFromKeyValue("opt4") | ||||||
|  |  | ||||||
|  | 	assert.Equal(t, "a=b", o["opt1"]) | ||||||
|  | 	assert.False(t, o.Bool("opt1").Value()) | ||||||
|  | 	assert.True(t, o.Bool("opt2").Has()) | ||||||
|  | 	assert.False(t, o.Bool("opt2").Value()) | ||||||
|  | 	assert.True(t, o.Bool("opt3").Value()) | ||||||
|  | 	assert.True(t, o.Bool("opt4").Value()) | ||||||
|  | } | ||||||
| @@ -208,7 +208,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) { | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		cols := make([]string, 0, len(opts.GitPushOptions)) | 		cols := make([]string, 0, 2) | ||||||
|  |  | ||||||
| 		if isPrivate.Has() { | 		if isPrivate.Has() { | ||||||
| 			repo.IsPrivate = isPrivate.Value() | 			repo.IsPrivate = isPrivate.Value() | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" |  | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
| 	issues_model "code.gitea.io/gitea/models/issues" | 	issues_model "code.gitea.io/gitea/models/issues" | ||||||
| @@ -24,10 +23,10 @@ import ( | |||||||
| // ProcReceive handle proc receive work | // ProcReceive handle proc receive work | ||||||
| func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { | func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) { | ||||||
| 	results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) | 	results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs)) | ||||||
|  | 	forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush) | ||||||
| 	topicBranch := opts.GitPushOptions["topic"] | 	topicBranch := opts.GitPushOptions["topic"] | ||||||
| 	forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"]) |  | ||||||
| 	title := strings.TrimSpace(opts.GitPushOptions["title"]) | 	title := strings.TrimSpace(opts.GitPushOptions["title"]) | ||||||
| 	description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options? | 	description := strings.TrimSpace(opts.GitPushOptions["description"]) | ||||||
| 	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) | 	objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName) | ||||||
| 	userName := strings.ToLower(opts.UserName) | 	userName := strings.ToLower(opts.UserName) | ||||||
|  |  | ||||||
| @@ -56,19 +55,19 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. | |||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		baseBranchName := opts.RefFullNames[i].ForBranchName() | 		baseBranchName := opts.RefFullNames[i].ForBranchName() | ||||||
| 		curentTopicBranch := "" | 		currentTopicBranch := "" | ||||||
| 		if !gitRepo.IsBranchExist(baseBranchName) { | 		if !gitRepo.IsBranchExist(baseBranchName) { | ||||||
| 			// try match refs/for/<target-branch>/<topic-branch> | 			// try match refs/for/<target-branch>/<topic-branch> | ||||||
| 			for p, v := range baseBranchName { | 			for p, v := range baseBranchName { | ||||||
| 				if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { | 				if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 { | ||||||
| 					curentTopicBranch = baseBranchName[p+1:] | 					currentTopicBranch = baseBranchName[p+1:] | ||||||
| 					baseBranchName = baseBranchName[:p] | 					baseBranchName = baseBranchName[:p] | ||||||
| 					break | 					break | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(topicBranch) == 0 && len(curentTopicBranch) == 0 { | 		if len(topicBranch) == 0 && len(currentTopicBranch) == 0 { | ||||||
| 			results = append(results, private.HookProcReceiveRefResult{ | 			results = append(results, private.HookProcReceiveRefResult{ | ||||||
| 				OriginalRef: opts.RefFullNames[i], | 				OriginalRef: opts.RefFullNames[i], | ||||||
| 				OldOID:      opts.OldCommitIDs[i], | 				OldOID:      opts.OldCommitIDs[i], | ||||||
| @@ -78,18 +77,18 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if len(curentTopicBranch) == 0 { | 		if len(currentTopicBranch) == 0 { | ||||||
| 			curentTopicBranch = topicBranch | 			currentTopicBranch = topicBranch | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// because different user maybe want to use same topic, | 		// because different user maybe want to use same topic, | ||||||
| 		// So it's better to make sure the topic branch name | 		// So it's better to make sure the topic branch name | ||||||
| 		// has username prefix | 		// has username prefix | ||||||
| 		var headBranch string | 		var headBranch string | ||||||
| 		if !strings.HasPrefix(curentTopicBranch, userName+"/") { | 		if !strings.HasPrefix(currentTopicBranch, userName+"/") { | ||||||
| 			headBranch = userName + "/" + curentTopicBranch | 			headBranch = userName + "/" + currentTopicBranch | ||||||
| 		} else { | 		} else { | ||||||
| 			headBranch = curentTopicBranch | 			headBranch = currentTopicBranch | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit) | 		pr, err := issues_model.GetUnmergedPullRequest(ctx, repo.ID, repo.ID, headBranch, baseBranchName, issues_model.PullRequestFlowAGit) | ||||||
| @@ -178,7 +177,7 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git. | |||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		if !forcePush { | 		if !forcePush.Value() { | ||||||
| 			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1"). | 			output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1"). | ||||||
| 				AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]). | 				AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]). | ||||||
| 				RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) | 				RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()}) | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ package integration | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"bytes" | 	"bytes" | ||||||
|  | 	"context" | ||||||
| 	"crypto/rand" | 	"crypto/rand" | ||||||
| 	"encoding/hex" | 	"encoding/hex" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| @@ -943,3 +944,59 @@ func TestDataAsync_Issue29101(t *testing.T) { | |||||||
| 		defer r2.Close() | 		defer r2.Close() | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func TestAgitPullPush(t *testing.T) { | ||||||
|  | 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||||||
|  | 		baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) | ||||||
|  |  | ||||||
|  | 		u.Path = baseAPITestContext.GitPath() | ||||||
|  | 		u.User = url.UserPassword("user2", userPassword) | ||||||
|  |  | ||||||
|  | 		dstPath := t.TempDir() | ||||||
|  | 		doGitClone(dstPath, u)(t) | ||||||
|  |  | ||||||
|  | 		gitRepo, err := git.OpenRepository(context.Background(), dstPath) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 		defer gitRepo.Close() | ||||||
|  |  | ||||||
|  | 		doGitCreateBranch(dstPath, "test-agit-push") | ||||||
|  |  | ||||||
|  | 		// commit 1 | ||||||
|  | 		_, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		// push to create an agit pull request | ||||||
|  | 		err = git.NewCommand(git.DefaultContext, "push", "origin", | ||||||
|  | 			"-o", "title=test-title", "-o", "description=test-description", | ||||||
|  | 			"HEAD:refs/for/master/test-agit-push", | ||||||
|  | 		).Run(&git.RunOpts{Dir: dstPath}) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		// check pull request exist | ||||||
|  | 		pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"}) | ||||||
|  | 		assert.NoError(t, pr.LoadIssue(db.DefaultContext)) | ||||||
|  | 		assert.Equal(t, "test-title", pr.Issue.Title) | ||||||
|  | 		assert.Equal(t, "test-description", pr.Issue.Content) | ||||||
|  |  | ||||||
|  | 		// commit 2 | ||||||
|  | 		_, err = generateCommitWithNewData(littleSize, dstPath, "user2@example.com", "User Two", "branch-data-file-2-") | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		// push 2 | ||||||
|  | 		err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath}) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		// reset to first commit | ||||||
|  | 		err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath}) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
|  | 		// test force push without confirm | ||||||
|  | 		_, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath}) | ||||||
|  | 		assert.Error(t, err) | ||||||
|  | 		assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)") | ||||||
|  |  | ||||||
|  | 		// test force push with confirm | ||||||
|  | 		err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath}) | ||||||
|  | 		assert.NoError(t, err) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 wxiaoguang
					wxiaoguang