Skip to content

Commit afa8dd4

Browse files
authored
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`
1 parent 900ac62 commit afa8dd4

File tree

7 files changed

+149
-44
lines changed

7 files changed

+149
-44
lines changed

cmd/hook.go

+4-9
Original file line numberDiff line numberDiff line change
@@ -591,8 +591,9 @@ Gitea or set your environment appropriately.`, "")
591591
// S: ... ...
592592
// S: flush-pkt
593593
hookOptions := private.HookOptions{
594-
UserName: pusherName,
595-
UserID: pusherID,
594+
UserName: pusherName,
595+
UserID: pusherID,
596+
GitPushOptions: make(map[string]string),
596597
}
597598
hookOptions.OldCommitIDs = make([]string, 0, hookBatchSize)
598599
hookOptions.NewCommitIDs = make([]string, 0, hookBatchSize)
@@ -617,8 +618,6 @@ Gitea or set your environment appropriately.`, "")
617618
hookOptions.RefFullNames = append(hookOptions.RefFullNames, git.RefName(t[2]))
618619
}
619620

620-
hookOptions.GitPushOptions = make(map[string]string)
621-
622621
if hasPushOptions {
623622
for {
624623
rs, err = readPktLine(ctx, reader, pktLineTypeUnknow)
@@ -629,11 +628,7 @@ Gitea or set your environment appropriately.`, "")
629628
if rs.Type == pktLineTypeFlush {
630629
break
631630
}
632-
633-
kv := strings.SplitN(string(rs.Data), "=", 2)
634-
if len(kv) == 2 {
635-
hookOptions.GitPushOptions[kv[0]] = kv[1]
636-
}
631+
hookOptions.GitPushOptions.AddFromKeyValue(string(rs.Data))
637632
}
638633
}
639634

modules/private/hook.go

-21
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,9 @@ import (
77
"context"
88
"fmt"
99
"net/url"
10-
"strconv"
1110
"time"
1211

1312
"code.gitea.io/gitea/modules/git"
14-
"code.gitea.io/gitea/modules/optional"
1513
"code.gitea.io/gitea/modules/repository"
1614
"code.gitea.io/gitea/modules/setting"
1715
)
@@ -24,25 +22,6 @@ const (
2422
GitPushOptionCount = "GIT_PUSH_OPTION_COUNT"
2523
)
2624

27-
// GitPushOptions is a wrapper around a map[string]string
28-
type GitPushOptions map[string]string
29-
30-
// GitPushOptions keys
31-
const (
32-
GitPushOptionRepoPrivate = "repo.private"
33-
GitPushOptionRepoTemplate = "repo.template"
34-
)
35-
36-
// Bool checks for a key in the map and parses as a boolean
37-
func (g GitPushOptions) Bool(key string) optional.Option[bool] {
38-
if val, ok := g[key]; ok {
39-
if b, err := strconv.ParseBool(val); err == nil {
40-
return optional.Some(b)
41-
}
42-
}
43-
return optional.None[bool]()
44-
}
45-
4625
// HookOptions represents the options for the Hook calls
4726
type HookOptions struct {
4827
OldCommitIDs []string

modules/private/pushoptions.go

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package private
5+
6+
import (
7+
"strconv"
8+
"strings"
9+
10+
"code.gitea.io/gitea/modules/optional"
11+
)
12+
13+
// GitPushOptions is a wrapper around a map[string]string
14+
type GitPushOptions map[string]string
15+
16+
// GitPushOptions keys
17+
const (
18+
GitPushOptionRepoPrivate = "repo.private"
19+
GitPushOptionRepoTemplate = "repo.template"
20+
GitPushOptionForcePush = "force-push"
21+
)
22+
23+
// Bool checks for a key in the map and parses as a boolean
24+
// An option without value is considered true, eg: "-o force-push" or "-o repo.private"
25+
func (g GitPushOptions) Bool(key string) optional.Option[bool] {
26+
if val, ok := g[key]; ok {
27+
if val == "" {
28+
return optional.Some(true)
29+
}
30+
if b, err := strconv.ParseBool(val); err == nil {
31+
return optional.Some(b)
32+
}
33+
}
34+
return optional.None[bool]()
35+
}
36+
37+
// AddFromKeyValue adds a key value pair to the map by "key=value" format or "key" for empty value
38+
func (g GitPushOptions) AddFromKeyValue(line string) {
39+
kv := strings.SplitN(line, "=", 2)
40+
if len(kv) == 2 {
41+
g[kv[0]] = kv[1]
42+
} else {
43+
g[kv[0]] = ""
44+
}
45+
}

modules/private/pushoptions_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2024 The Gitea Authors. All rights reserved.
2+
// SPDX-License-Identifier: MIT
3+
4+
package private
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestGitPushOptions(t *testing.T) {
13+
o := GitPushOptions{}
14+
15+
v := o.Bool("no-such")
16+
assert.False(t, v.Has())
17+
assert.False(t, v.Value())
18+
19+
o.AddFromKeyValue("opt1=a=b")
20+
o.AddFromKeyValue("opt2=false")
21+
o.AddFromKeyValue("opt3=true")
22+
o.AddFromKeyValue("opt4")
23+
24+
assert.Equal(t, "a=b", o["opt1"])
25+
assert.False(t, o.Bool("opt1").Value())
26+
assert.True(t, o.Bool("opt2").Has())
27+
assert.False(t, o.Bool("opt2").Value())
28+
assert.True(t, o.Bool("opt3").Value())
29+
assert.True(t, o.Bool("opt4").Value())
30+
}

routers/private/hook_post_receive.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ func HookPostReceive(ctx *gitea_context.PrivateContext) {
208208
return
209209
}
210210

211-
cols := make([]string, 0, len(opts.GitPushOptions))
211+
cols := make([]string, 0, 2)
212212

213213
if isPrivate.Has() {
214214
repo.IsPrivate = isPrivate.Value()

services/agit/agit.go

+12-13
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"context"
88
"fmt"
99
"os"
10-
"strconv"
1110
"strings"
1211

1312
issues_model "code.gitea.io/gitea/models/issues"
@@ -24,10 +23,10 @@ import (
2423
// ProcReceive handle proc receive work
2524
func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, opts *private.HookOptions) ([]private.HookProcReceiveRefResult, error) {
2625
results := make([]private.HookProcReceiveRefResult, 0, len(opts.OldCommitIDs))
26+
forcePush := opts.GitPushOptions.Bool(private.GitPushOptionForcePush)
2727
topicBranch := opts.GitPushOptions["topic"]
28-
forcePush, _ := strconv.ParseBool(opts.GitPushOptions["force-push"])
2928
title := strings.TrimSpace(opts.GitPushOptions["title"])
30-
description := strings.TrimSpace(opts.GitPushOptions["description"]) // TODO: Add more options?
29+
description := strings.TrimSpace(opts.GitPushOptions["description"])
3130
objectFormat := git.ObjectFormatFromName(repo.ObjectFormatName)
3231
userName := strings.ToLower(opts.UserName)
3332

@@ -56,19 +55,19 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
5655
}
5756

5857
baseBranchName := opts.RefFullNames[i].ForBranchName()
59-
curentTopicBranch := ""
58+
currentTopicBranch := ""
6059
if !gitRepo.IsBranchExist(baseBranchName) {
6160
// try match refs/for/<target-branch>/<topic-branch>
6261
for p, v := range baseBranchName {
6362
if v == '/' && gitRepo.IsBranchExist(baseBranchName[:p]) && p != len(baseBranchName)-1 {
64-
curentTopicBranch = baseBranchName[p+1:]
63+
currentTopicBranch = baseBranchName[p+1:]
6564
baseBranchName = baseBranchName[:p]
6665
break
6766
}
6867
}
6968
}
7069

71-
if len(topicBranch) == 0 && len(curentTopicBranch) == 0 {
70+
if len(topicBranch) == 0 && len(currentTopicBranch) == 0 {
7271
results = append(results, private.HookProcReceiveRefResult{
7372
OriginalRef: opts.RefFullNames[i],
7473
OldOID: opts.OldCommitIDs[i],
@@ -78,18 +77,18 @@ func ProcReceive(ctx context.Context, repo *repo_model.Repository, gitRepo *git.
7877
continue
7978
}
8079

81-
if len(curentTopicBranch) == 0 {
82-
curentTopicBranch = topicBranch
80+
if len(currentTopicBranch) == 0 {
81+
currentTopicBranch = topicBranch
8382
}
8483

8584
// because different user maybe want to use same topic,
8685
// So it's better to make sure the topic branch name
87-
// has user name prefix
86+
// has username prefix
8887
var headBranch string
89-
if !strings.HasPrefix(curentTopicBranch, userName+"/") {
90-
headBranch = userName + "/" + curentTopicBranch
88+
if !strings.HasPrefix(currentTopicBranch, userName+"/") {
89+
headBranch = userName + "/" + currentTopicBranch
9190
} else {
92-
headBranch = curentTopicBranch
91+
headBranch = currentTopicBranch
9392
}
9493

9594
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.
178177
continue
179178
}
180179

181-
if !forcePush {
180+
if !forcePush.Value() {
182181
output, _, err := git.NewCommand(ctx, "rev-list", "--max-count=1").
183182
AddDynamicArguments(oldCommitID, "^"+opts.NewCommitIDs[i]).
184183
RunStdString(&git.RunOpts{Dir: repo.RepoPath(), Env: os.Environ()})

tests/integration/git_test.go

+57
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package integration
55

66
import (
77
"bytes"
8+
"context"
89
"crypto/rand"
910
"encoding/hex"
1011
"fmt"
@@ -943,3 +944,59 @@ func TestDataAsync_Issue29101(t *testing.T) {
943944
defer r2.Close()
944945
})
945946
}
947+
948+
func TestAgitPullPush(t *testing.T) {
949+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
950+
baseAPITestContext := NewAPITestContext(t, "user2", "repo1", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser)
951+
952+
u.Path = baseAPITestContext.GitPath()
953+
u.User = url.UserPassword("user2", userPassword)
954+
955+
dstPath := t.TempDir()
956+
doGitClone(dstPath, u)(t)
957+
958+
gitRepo, err := git.OpenRepository(context.Background(), dstPath)
959+
assert.NoError(t, err)
960+
defer gitRepo.Close()
961+
962+
doGitCreateBranch(dstPath, "test-agit-push")
963+
964+
// commit 1
965+
_, err = generateCommitWithNewData(littleSize, dstPath, "[email protected]", "User Two", "branch-data-file-")
966+
assert.NoError(t, err)
967+
968+
// push to create an agit pull request
969+
err = git.NewCommand(git.DefaultContext, "push", "origin",
970+
"-o", "title=test-title", "-o", "description=test-description",
971+
"HEAD:refs/for/master/test-agit-push",
972+
).Run(&git.RunOpts{Dir: dstPath})
973+
assert.NoError(t, err)
974+
975+
// check pull request exist
976+
pr := unittest.AssertExistsAndLoadBean(t, &issues_model.PullRequest{BaseRepoID: 1, Flow: issues_model.PullRequestFlowAGit, HeadBranch: "user2/test-agit-push"})
977+
assert.NoError(t, pr.LoadIssue(db.DefaultContext))
978+
assert.Equal(t, "test-title", pr.Issue.Title)
979+
assert.Equal(t, "test-description", pr.Issue.Content)
980+
981+
// commit 2
982+
_, err = generateCommitWithNewData(littleSize, dstPath, "[email protected]", "User Two", "branch-data-file-2-")
983+
assert.NoError(t, err)
984+
985+
// push 2
986+
err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").Run(&git.RunOpts{Dir: dstPath})
987+
assert.NoError(t, err)
988+
989+
// reset to first commit
990+
err = git.NewCommand(git.DefaultContext, "reset", "--hard", "HEAD~1").Run(&git.RunOpts{Dir: dstPath})
991+
assert.NoError(t, err)
992+
993+
// test force push without confirm
994+
_, stderr, err := git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push").RunStdString(&git.RunOpts{Dir: dstPath})
995+
assert.Error(t, err)
996+
assert.Contains(t, stderr, "[remote rejected] HEAD -> refs/for/master/test-agit-push (request `force-push` push option)")
997+
998+
// test force push with confirm
999+
err = git.NewCommand(git.DefaultContext, "push", "origin", "HEAD:refs/for/master/test-agit-push", "-o", "force-push").Run(&git.RunOpts{Dir: dstPath})
1000+
assert.NoError(t, err)
1001+
})
1002+
}

0 commit comments

Comments
 (0)