diff --git a/pkg/commands/git_commands/remote_loader.go b/pkg/commands/git_commands/remote_loader.go index 164ab0016..2daeb600d 100644 --- a/pkg/commands/git_commands/remote_loader.go +++ b/pkg/commands/git_commands/remote_loader.go @@ -69,7 +69,7 @@ func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) { func (self *RemoteLoader) getRemotesFromConfig() []*models.Remote { cmdArgs := NewGitCmd("config"). - Arg("--local", "--get-regexp", `^remote\.[^.]+\.url$`).ToArgv() + Arg("--local", "--get-regexp", `^remote\.[^.]+\.(url|pushurl)$`).ToArgv() output, err := self.cmd.New(cmdArgs).DontLog().RunWithOutput() if err != nil { // exit code 1 means no matching keys (no remotes configured) @@ -83,12 +83,29 @@ func (self *RemoteLoader) getRemotesFromConfig() []*models.Remote { if !found { continue } - // key is "remote..url"; strip prefix and suffix to get the name - remoteName := strings.TrimSuffix(strings.TrimPrefix(key, "remote."), ".url") + // key is "remote..url" or "remote..pushurl"; + // strip prefix and suffix to get the name + rest, ok := strings.CutPrefix(key, "remote.") + if !ok { + continue + } + var remoteName string + var isPushUrl bool + if name, ok := strings.CutSuffix(rest, ".pushurl"); ok { + remoteName, isPushUrl = name, true + } else if name, ok := strings.CutSuffix(rest, ".url"); ok { + remoteName, isPushUrl = name, false + } else { + continue + } if _, ok := remotesByName[remoteName]; !ok { remotesByName[remoteName] = &models.Remote{Name: remoteName} } - remotesByName[remoteName].Urls = append(remotesByName[remoteName].Urls, url) + if isPushUrl { + remotesByName[remoteName].PushUrls = append(remotesByName[remoteName].PushUrls, url) + } else { + remotesByName[remoteName].Urls = append(remotesByName[remoteName].Urls, url) + } } return slices.Collect(maps.Values(remotesByName)) diff --git a/pkg/commands/git_commands/remote_loader_test.go b/pkg/commands/git_commands/remote_loader_test.go new file mode 100644 index 000000000..3d4ed4d12 --- /dev/null +++ b/pkg/commands/git_commands/remote_loader_test.go @@ -0,0 +1,108 @@ +package git_commands + +import ( + "errors" + "testing" + + "github.com/jesseduffield/lazygit/pkg/commands/models" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/common" + "github.com/stretchr/testify/assert" +) + +func TestGetRemotesFromConfig(t *testing.T) { + configArgs := []string{"config", "--local", "--get-regexp", `^remote\.[^.]+\.(url|pushurl)$`} + + scenarios := []struct { + testName string + runner *oscommands.FakeCmdObjRunner + expectedRemotes []*models.Remote + }{ + { + testName: "no remotes configured", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, "", errors.New("exit status 1")), + expectedRemotes: nil, + }, + { + testName: "single remote with one url", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, + "remote.origin.url https://github.com/foo/bar.git\n", + nil), + expectedRemotes: []*models.Remote{ + {Name: "origin", Urls: []string{"https://github.com/foo/bar.git"}}, + }, + }, + { + testName: "mirror remote with multiple urls", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, + "remote.origin.url https://github.com/foo/bar.git\n"+ + "remote.origin.url git@github.com:foo/bar.git\n", + nil), + expectedRemotes: []*models.Remote{ + {Name: "origin", Urls: []string{ + "https://github.com/foo/bar.git", + "git@github.com:foo/bar.git", + }}, + }, + }, + { + testName: "remote with both url and pushurl", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, + "remote.origin.url https://github.com/foo/bar.git\n"+ + "remote.origin.pushurl git@github.com:foo/bar.git\n", + nil), + expectedRemotes: []*models.Remote{ + { + Name: "origin", + Urls: []string{"https://github.com/foo/bar.git"}, + PushUrls: []string{"git@github.com:foo/bar.git"}, + }, + }, + }, + { + testName: "multiple remotes", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, + "remote.origin.url https://github.com/foo/bar.git\n"+ + "remote.upstream.url https://github.com/baz/bar.git\n"+ + "remote.upstream.pushurl git@github.com:baz/bar.git\n", + nil), + expectedRemotes: []*models.Remote{ + {Name: "origin", Urls: []string{"https://github.com/foo/bar.git"}}, + { + Name: "upstream", + Urls: []string{"https://github.com/baz/bar.git"}, + PushUrls: []string{"git@github.com:baz/bar.git"}, + }, + }, + }, + { + testName: "remote name containing dots is preserved", + runner: oscommands.NewFakeRunner(t). + ExpectGitArgs(configArgs, + "remote.my.fork.url https://github.com/foo/bar.git\n", + nil), + expectedRemotes: []*models.Remote{ + {Name: "my.fork", Urls: []string{"https://github.com/foo/bar.git"}}, + }, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.testName, func(t *testing.T) { + loader := &RemoteLoader{ + Common: common.NewDummyCommon(), + cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner), + } + + // map iteration order is non-deterministic, so compare unordered + assert.ElementsMatch(t, scenario.expectedRemotes, loader.getRemotesFromConfig()) + + scenario.runner.CheckForMissingCalls() + }) + } +} diff --git a/pkg/commands/models/remote.go b/pkg/commands/models/remote.go index 418b45833..68ec3bd71 100644 --- a/pkg/commands/models/remote.go +++ b/pkg/commands/models/remote.go @@ -2,8 +2,11 @@ package models // Remote : A git remote type Remote struct { - Name string - Urls []string + Name string + Urls []string + // PushUrls is empty unless the remote has explicit `remote..pushurl` + // entries; when empty, pushes go to Urls. + PushUrls []string Branches []*RemoteBranch }