Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ import (
forges "github.com/git-pkgs/forge"
"github.com/git-pkgs/forge/internal/config"
"github.com/git-pkgs/forge/internal/output"
"github.com/git-pkgs/forge/internal/resolve"
"github.com/spf13/cobra"
)

var (
flagRepo string
flagForgeType string
flagOutput string
flagRemote string
)

var rootCmd = &cobra.Command{
Expand All @@ -29,6 +31,7 @@ var rootCmd = &cobra.Command{
flagOutput = cfg.Default.Output
}
}
resolve.SetRemote(flagRemote)
},
}

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

// notSupported wraps ErrNotSupported with a user-friendly message
Expand Down
34 changes: 27 additions & 7 deletions internal/resolve/resolve.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ import (

const ownerRepoParts = 2

var remoteName = "origin"

// SetRemote sets which git remote to read when resolving the current
// repository. The CLI calls this from the --remote persistent flag.
// An empty string is ignored so callers can pass the flag value
// unconditionally.
func SetRemote(name string) {
if name != "" {
remoteName = name
}
}

var builders = forges.ForgeBuilders{
GitHub: ghforge.NewWithBase,
GitLab: glforge.New,
Expand Down Expand Up @@ -50,14 +62,9 @@ func repoFromFlag(flagRepo, flagForgeType string) (forges.Forge, string, string,
}

func repoFromGitRemote(_ string) (forges.Forge, string, string, string, error) {
url, err := gitRemoteURL("origin")
domain, owner, repo, err := resolveRemote()
if err != nil {
return nil, "", "", "", fmt.Errorf("not in a git repo and -R not set: %w", err)
}

domain, owner, repo, err := forges.ParseRepoURL(url)
if err != nil {
return nil, "", "", "", fmt.Errorf("parsing remote URL: %w", err)
return nil, "", "", "", err
}

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

func resolveRemote() (domain, owner, repo string, err error) {
url, err := gitRemoteURL(remoteName)
if err != nil {
return "", "", "", fmt.Errorf("reading remote %q (not in a git repo, or remote not configured; use -R or --remote): %w", remoteName, err)
}

domain, owner, repo, err = forges.ParseRepoURL(url)
if err != nil {
return "", "", "", fmt.Errorf("parsing remote %q URL: %w", remoteName, err)
}
return domain, owner, repo, nil
}

func gitRemoteURL(name string) (string, error) {
out, err := exec.Command("git", "remote", "get-url", name).Output()
if err != nil {
Expand Down
106 changes: 106 additions & 0 deletions internal/resolve/resolve_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package resolve

import (
"os"
"os/exec"
"strings"
"testing"
)

Expand Down Expand Up @@ -106,3 +109,106 @@ func TestDomainFromForgeTypeWithForgeHost(t *testing.T) {
t.Errorf("expected FORGE_HOST override for empty type, got %q", got)
}
}

func TestRemoteDefaultsToOrigin(t *testing.T) {
if remoteName != "origin" {
t.Errorf("default remote should be origin, got %q", remoteName)
}
}

func TestSetRemote(t *testing.T) {
old := remoteName
defer func() { remoteName = old }()

SetRemote("upstream")
if remoteName != "upstream" {
t.Errorf("SetRemote did not update remoteName, got %q", remoteName)
}

// Empty string should leave the default alone so callers can pass
// a flag value unconditionally without resetting to "".
SetRemote("")
if remoteName != "upstream" {
t.Errorf("SetRemote(\"\") should be a no-op, got %q", remoteName)
}
}

func TestRemoteSelectsCorrectGitURL(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}

dir := t.TempDir()
t.Chdir(dir)

mustGit(t, "init", "-q")
mustGit(t, "remote", "add", "origin", "https://gitea.example.com/owner/origin-repo.git")
mustGit(t, "remote", "add", "mirror", "https://github.com/owner/mirror-repo.git")

old := remoteName
defer func() { remoteName = old }()

tests := []struct {
remote string
wantDomain string
wantRepo string
}{
{"origin", "gitea.example.com", "origin-repo"},
{"mirror", "github.com", "mirror-repo"},
}

for _, tt := range tests {
t.Run(tt.remote, func(t *testing.T) {
SetRemote(tt.remote)
domain, owner, repo, err := resolveRemote()
if err != nil {
t.Fatalf("resolveRemote: %v", err)
}
if domain != tt.wantDomain {
t.Errorf("domain = %q, want %q", domain, tt.wantDomain)
}
if owner != "owner" {
t.Errorf("owner = %q, want owner", owner)
}
if repo != tt.wantRepo {
t.Errorf("repo = %q, want %q", repo, tt.wantRepo)
}
})
}
}

func TestRemoteUnknownNameError(t *testing.T) {
if _, err := exec.LookPath("git"); err != nil {
t.Skip("git not installed")
}

dir := t.TempDir()
t.Chdir(dir)

mustGit(t, "init", "-q")
mustGit(t, "remote", "add", "origin", "https://github.com/owner/repo.git")

old := remoteName
defer func() { remoteName = old }()

SetRemote("doesnotexist")
_, _, _, err := resolveRemote()
if err == nil {
t.Fatal("expected error for unknown remote")
}
if !strings.Contains(err.Error(), "doesnotexist") {
t.Errorf("error should mention the remote name, got: %v", err)
}
}

func mustGit(t *testing.T, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Env = append(os.Environ(),
"GIT_CONFIG_GLOBAL=/dev/null",
"GIT_CONFIG_SYSTEM=/dev/null",
)
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("git %v: %v\n%s", args, err, out)
}
}