Find push remote using branch.<name>.pushRemote and remote.pushDefault

When using a push.default = current triangular workflow, apart from
using @{push} to determine the remote branch name, we should also follow
the

1. branch.<name>.pushRemote
2. remote.pushDefault
3. branch.<name>.remote

...list to determine which remote Git pushes to.
This commit is contained in:
Frederick Zhang 2024-06-14 19:00:31 +10:00
parent 7fc35fd47d
commit 4254818dbd
No known key found for this signature in database
GPG key ID: 980A192C361BE1AE
8 changed files with 215 additions and 37 deletions

View file

@ -379,7 +379,7 @@ func (c *Client) lookupCommit(ctx context.Context, sha, format string) ([]byte,
// ReadBranchConfig parses the `branch.BRANCH.(remote|merge|gh-merge-base)` part of git config.
func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg BranchConfig) {
prefix := regexp.QuoteMeta(fmt.Sprintf("branch.%s.", branch))
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|%s)$", prefix, MergeBaseConfig)}
args := []string{"config", "--get-regexp", fmt.Sprintf("^%s(remote|merge|pushremote|%s)$", prefix, MergeBaseConfig)}
cmd, err := c.Command(ctx, args...)
if err != nil {
return
@ -397,21 +397,22 @@ func (c *Client) ReadBranchConfig(ctx context.Context, branch string) (cfg Branc
keys := strings.Split(parts[0], ".")
switch keys[len(keys)-1] {
case "remote":
if strings.Contains(parts[1], ":") {
u, err := ParseURL(parts[1])
if err != nil {
continue
}
cfg.RemoteURL = u
} else if !isFilesystemPath(parts[1]) {
cfg.RemoteName = parts[1]
}
parseRemoteURLOrName(parts[1], &cfg.RemoteURL, &cfg.RemoteName)
case "pushremote":
parseRemoteURLOrName(parts[1], &cfg.PushRemoteURL, &cfg.PushRemoteName)
case "merge":
cfg.MergeRef = parts[1]
case MergeBaseConfig:
cfg.MergeBase = parts[1]
}
}
if cfg.PushRemoteURL == nil && cfg.PushRemoteName == "" {
if conf, err := c.Config(ctx, "remote.pushDefault"); err == nil && conf != "" {
parseRemoteURLOrName(conf, &cfg.PushRemoteURL, &cfg.PushRemoteName)
} else {
cfg.PushRemoteName = cfg.RemoteName
}
}
if out, err = c.revParse(ctx, "--verify", "--quiet", "--abbrev-ref", branch+"@{push}"); err == nil {
cfg.Push = strings.TrimSuffix(string(out), "\n")
}
@ -776,6 +777,16 @@ func parseRemotes(remotesStr []string) RemoteSet {
return remotes
}
func parseRemoteURLOrName(value string, remoteURL **url.URL, remoteName *string) {
if strings.Contains(value, ":") {
if u, err := ParseURL(value); err == nil {
*remoteURL = u
}
} else if !isFilesystemPath(value) {
*remoteName = value
}
}
func populateResolvedRemotes(remotes RemoteSet, resolved []string) {
for _, l := range resolved {
parts := strings.SplitN(l, " ", 2)

View file

@ -725,31 +725,160 @@ func TestClientCommitBody(t *testing.T) {
}
func TestClientReadBranchConfig(t *testing.T) {
type cmdTest struct {
exitStatus int
stdOut string
stdErr string
wantArgs string
}
tests := []struct {
name string
cmdExitStatus []int
cmdStdout []string
cmdStderr []string
wantCmdArgs []string
cmds []cmdTest
wantBranchConfig BranchConfig
}{
{
name: "read branch config",
cmdExitStatus: []int{0, 0},
cmdStdout: []string{"branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk", "origin/trunk"},
cmdStderr: []string{"", ""},
wantCmdArgs: []string{`path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|gh-merge-base)$`, `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`},
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk", Push: "origin/trunk"},
name: "read branch config, central",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk\nbranch.trunk.gh-merge-base trunk",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
wantArgs: `path/to/git config remote.pushDefault`,
},
{
stdOut: "origin/trunk",
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk", MergeBase: "trunk", PushRemoteName: "origin", Push: "origin/trunk"},
},
{
name: "read branch config, central, push.default = upstream",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/trunk-remote",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
wantArgs: `path/to/git config remote.pushDefault`,
},
{
stdOut: "origin/trunk-remote",
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/trunk-remote", PushRemoteName: "origin", Push: "origin/trunk-remote"},
},
{
name: "read branch config, central, push.default = current",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote origin\nbranch.trunk.merge refs/heads/main",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
wantArgs: `path/to/git config remote.pushDefault`,
},
{
stdOut: "origin/trunk",
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "origin", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"},
},
{
name: "read branch config, central, push.default = current, target branch not pushed, no existing remote branch",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote .\nbranch.trunk.merge refs/heads/trunk-middle",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
stdOut: "origin",
wantArgs: `path/to/git config remote.pushDefault`,
},
{
exitStatus: 1,
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{MergeRef: "refs/heads/trunk-middle", PushRemoteName: "origin"},
},
{
name: "read branch config, triangular, push.default = current, has existing remote branch, branch.trunk.pushremote effective",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
stdOut: "origin/trunk",
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"},
},
{
name: "read branch config, triangular, push.default = current, has existing remote branch, remote.pushDefault effective",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
stdOut: "origin",
wantArgs: `path/to/git config remote.pushDefault`,
},
{
stdOut: "origin/trunk",
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin", Push: "origin/trunk"},
},
{
name: "read branch config, triangular, push.default = current, no existing remote branch, branch.trunk.pushremote effective",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main\nbranch.trunk.pushremote origin",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
exitStatus: 1,
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin"},
},
{
name: "read branch config, triangular, push.default = current, no existing remote branch, remote.pushDefault effective",
cmds: []cmdTest{
{
stdOut: "branch.trunk.remote upstream\nbranch.trunk.merge refs/heads/main",
wantArgs: `path/to/git config --get-regexp ^branch\.trunk\.(remote|merge|pushremote|gh-merge-base)$`,
},
{
stdOut: "origin",
wantArgs: `path/to/git config remote.pushDefault`,
},
{
exitStatus: 1,
wantArgs: `path/to/git rev-parse --verify --quiet --abbrev-ref trunk@{push}`,
},
},
wantBranchConfig: BranchConfig{RemoteName: "upstream", MergeRef: "refs/heads/main", PushRemoteName: "origin"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var cmds []*exec.Cmd
var cmdCtxs []commandCtx
for i := 0; i < len(tt.cmdExitStatus); i++ {
cmd, cmdCtx := createCommandContext(t, tt.cmdExitStatus[i], tt.cmdStdout[i], tt.cmdStderr[i])
for _, c := range tt.cmds {
cmd, cmdCtx := createCommandContext(t, c.exitStatus, c.stdOut, c.stdErr)
cmds = append(cmds, cmd)
cmdCtxs = append(cmdCtxs, cmdCtx)
}
i := -1
client := Client{
@ -761,8 +890,8 @@ func TestClientReadBranchConfig(t *testing.T) {
},
}
branchConfig := client.ReadBranchConfig(context.Background(), "trunk")
for i := 0; i < len(tt.cmdExitStatus); i++ {
assert.Equal(t, tt.wantCmdArgs[i], strings.Join(cmds[i].Args[3:], " "))
for i := 0; i < len(tt.cmds); i++ {
assert.Equal(t, tt.cmds[i].wantArgs, strings.Join(cmds[i].Args[3:], " "))
}
assert.Equal(t, tt.wantBranchConfig, branchConfig)
})

View file

@ -64,7 +64,9 @@ type BranchConfig struct {
RemoteName string
RemoteURL *url.URL
// MergeBase is the optional base branch to target in a new PR if `--base` is not specified.
MergeBase string
MergeRef string
Push string
MergeBase string
MergeRef string
PushRemoteURL *url.URL
PushRemoteName string
Push string
}