Skip to content

Commit 818dd54

Browse files
authored
Add --remote flag to select which git remote to resolve (#57)
Closes #42
1 parent 1bcb470 commit 818dd54

3 files changed

Lines changed: 136 additions & 7 deletions

File tree

internal/cli/root.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ var (
1616
flagRepo string
1717
flagForgeType string
1818
flagOutput string
19+
flagRemote string
1920
)
2021

2122
var rootCmd = &cobra.Command{
@@ -30,6 +31,7 @@ var rootCmd = &cobra.Command{
3031
flagOutput = cfg.Default.Output
3132
}
3233
}
34+
resolve.SetRemote(flagRemote)
3335
},
3436
}
3537

@@ -42,6 +44,7 @@ func init() {
4244
rootCmd.PersistentFlags().StringVarP(&flagRepo, "repo", "R", "", "Select a repository (OWNER/REPO)")
4345
rootCmd.PersistentFlags().StringVar(&flagForgeType, "forge-type", "", "Force forge type: github, gitlab, gitea, forgejo")
4446
rootCmd.PersistentFlags().StringVarP(&flagOutput, "output", "o", "table", "Output format: table, json, plain")
47+
rootCmd.PersistentFlags().StringVar(&flagRemote, "remote", "", "Git remote to use when not specifying -R (default origin)")
4548
}
4649

4750
// notSupported wraps ErrNotSupported with a user-friendly message

internal/resolve/resolve.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@ import (
1717

1818
const ownerRepoParts = 2
1919

20+
var remoteName = "origin"
21+
22+
// SetRemote sets which git remote to read when resolving the current
23+
// repository. The CLI calls this from the --remote persistent flag.
24+
// An empty string is ignored so callers can pass the flag value
25+
// unconditionally.
26+
func SetRemote(name string) {
27+
if name != "" {
28+
remoteName = name
29+
}
30+
}
31+
2032
var builders = forges.ForgeBuilders{
2133
GitHub: ghforge.NewWithBase,
2234
GitLab: glforge.New,
@@ -50,14 +62,9 @@ func repoFromFlag(flagRepo, flagForgeType string) (forges.Forge, string, string,
5062
}
5163

5264
func repoFromGitRemote(_ string) (forges.Forge, string, string, string, error) {
53-
url, err := gitRemoteURL("origin")
65+
domain, owner, repo, err := resolveRemote()
5466
if err != nil {
55-
return nil, "", "", "", fmt.Errorf("not in a git repo and -R not set: %w", err)
56-
}
57-
58-
domain, owner, repo, err := forges.ParseRepoURL(url)
59-
if err != nil {
60-
return nil, "", "", "", fmt.Errorf("parsing remote URL: %w", err)
67+
return nil, "", "", "", err
6168
}
6269

6370
client := newClient(domain)
@@ -68,6 +75,19 @@ func repoFromGitRemote(_ string) (forges.Forge, string, string, string, error) {
6875
return f, owner, repo, domain, nil
6976
}
7077

78+
func resolveRemote() (domain, owner, repo string, err error) {
79+
url, err := gitRemoteURL(remoteName)
80+
if err != nil {
81+
return "", "", "", fmt.Errorf("reading remote %q (not in a git repo, or remote not configured; use -R or --remote): %w", remoteName, err)
82+
}
83+
84+
domain, owner, repo, err = forges.ParseRepoURL(url)
85+
if err != nil {
86+
return "", "", "", fmt.Errorf("parsing remote %q URL: %w", remoteName, err)
87+
}
88+
return domain, owner, repo, nil
89+
}
90+
7191
func gitRemoteURL(name string) (string, error) {
7292
out, err := exec.Command("git", "remote", "get-url", name).Output()
7393
if err != nil {

internal/resolve/resolve_test.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package resolve
22

33
import (
4+
"os"
5+
"os/exec"
6+
"strings"
47
"testing"
58
)
69

@@ -106,3 +109,106 @@ func TestDomainFromForgeTypeWithForgeHost(t *testing.T) {
106109
t.Errorf("expected FORGE_HOST override for empty type, got %q", got)
107110
}
108111
}
112+
113+
func TestRemoteDefaultsToOrigin(t *testing.T) {
114+
if remoteName != "origin" {
115+
t.Errorf("default remote should be origin, got %q", remoteName)
116+
}
117+
}
118+
119+
func TestSetRemote(t *testing.T) {
120+
old := remoteName
121+
defer func() { remoteName = old }()
122+
123+
SetRemote("upstream")
124+
if remoteName != "upstream" {
125+
t.Errorf("SetRemote did not update remoteName, got %q", remoteName)
126+
}
127+
128+
// Empty string should leave the default alone so callers can pass
129+
// a flag value unconditionally without resetting to "".
130+
SetRemote("")
131+
if remoteName != "upstream" {
132+
t.Errorf("SetRemote(\"\") should be a no-op, got %q", remoteName)
133+
}
134+
}
135+
136+
func TestRemoteSelectsCorrectGitURL(t *testing.T) {
137+
if _, err := exec.LookPath("git"); err != nil {
138+
t.Skip("git not installed")
139+
}
140+
141+
dir := t.TempDir()
142+
t.Chdir(dir)
143+
144+
mustGit(t, "init", "-q")
145+
mustGit(t, "remote", "add", "origin", "https://gitea.example.com/owner/origin-repo.git")
146+
mustGit(t, "remote", "add", "mirror", "https://github.com/owner/mirror-repo.git")
147+
148+
old := remoteName
149+
defer func() { remoteName = old }()
150+
151+
tests := []struct {
152+
remote string
153+
wantDomain string
154+
wantRepo string
155+
}{
156+
{"origin", "gitea.example.com", "origin-repo"},
157+
{"mirror", "github.com", "mirror-repo"},
158+
}
159+
160+
for _, tt := range tests {
161+
t.Run(tt.remote, func(t *testing.T) {
162+
SetRemote(tt.remote)
163+
domain, owner, repo, err := resolveRemote()
164+
if err != nil {
165+
t.Fatalf("resolveRemote: %v", err)
166+
}
167+
if domain != tt.wantDomain {
168+
t.Errorf("domain = %q, want %q", domain, tt.wantDomain)
169+
}
170+
if owner != "owner" {
171+
t.Errorf("owner = %q, want owner", owner)
172+
}
173+
if repo != tt.wantRepo {
174+
t.Errorf("repo = %q, want %q", repo, tt.wantRepo)
175+
}
176+
})
177+
}
178+
}
179+
180+
func TestRemoteUnknownNameError(t *testing.T) {
181+
if _, err := exec.LookPath("git"); err != nil {
182+
t.Skip("git not installed")
183+
}
184+
185+
dir := t.TempDir()
186+
t.Chdir(dir)
187+
188+
mustGit(t, "init", "-q")
189+
mustGit(t, "remote", "add", "origin", "https://github.com/owner/repo.git")
190+
191+
old := remoteName
192+
defer func() { remoteName = old }()
193+
194+
SetRemote("doesnotexist")
195+
_, _, _, err := resolveRemote()
196+
if err == nil {
197+
t.Fatal("expected error for unknown remote")
198+
}
199+
if !strings.Contains(err.Error(), "doesnotexist") {
200+
t.Errorf("error should mention the remote name, got: %v", err)
201+
}
202+
}
203+
204+
func mustGit(t *testing.T, args ...string) {
205+
t.Helper()
206+
cmd := exec.Command("git", args...)
207+
cmd.Env = append(os.Environ(),
208+
"GIT_CONFIG_GLOBAL=/dev/null",
209+
"GIT_CONFIG_SYSTEM=/dev/null",
210+
)
211+
if out, err := cmd.CombinedOutput(); err != nil {
212+
t.Fatalf("git %v: %v\n%s", args, err, out)
213+
}
214+
}

0 commit comments

Comments
 (0)