diff --git a/README.md b/README.md index 08432cc..9a09a36 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ forge auth login --domain gitea.example.com --token abc123 --type gitea Check what's configured with `forge auth status`. -Tokens are resolved in this order: CLI flags, environment variables (`FORGE_TOKEN`, `GITHUB_TOKEN`/`GH_TOKEN`, `GITLAB_TOKEN`, `GITEA_TOKEN`, `BITBUCKET_TOKEN`), then the config file at `~/.config/forge/config`. Set `FORGE_HOST` to override which domain is used when there's no git remote to infer it from. +Tokens are resolved in this order: CLI flags, environment variables (`FORGE_TOKEN`, `GITHUB_TOKEN`/`GH_TOKEN`, `GITLAB_TOKEN`, `GITEA_TOKEN`, `BITBUCKET_TOKEN`), then the config file at `~/.config/forge/config`. The target host is inferred from the current directory's git remote; use `--host` or `FORGE_HOST` to override it (for example `forge --host gitea.com repo list someone`). ### Configuration diff --git a/internal/cli/repo.go b/internal/cli/repo.go index 898b58b..1edf80e 100644 --- a/internal/cli/repo.go +++ b/internal/cli/repo.go @@ -680,5 +680,5 @@ func repoContributorsCmd() *cobra.Command { } func domainFromFlags() string { - return resolve.DomainFromForgeType(flagForgeType) + return resolve.Domain(flagForgeType) } diff --git a/internal/cli/repo_test.go b/internal/cli/repo_test.go index 0cd8c24..3640212 100644 --- a/internal/cli/repo_test.go +++ b/internal/cli/repo_test.go @@ -85,6 +85,8 @@ func TestRepoEditMutuallyExclusiveVisibility(t *testing.T) { } func TestDomainFromFlags(t *testing.T) { + t.Chdir(t.TempDir()) + tests := []struct { forgeType string want string diff --git a/internal/cli/root.go b/internal/cli/root.go index 30c8527..25d54cd 100644 --- a/internal/cli/root.go +++ b/internal/cli/root.go @@ -15,6 +15,7 @@ import ( var ( flagRepo string flagForgeType string + flagHost string flagOutput string flagRemote string ) @@ -32,6 +33,7 @@ var rootCmd = &cobra.Command{ } } resolve.SetRemote(flagRemote) + resolve.SetHost(flagHost) }, } @@ -43,6 +45,7 @@ func Execute() error { 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().StringVar(&flagHost, "host", "", "Force forge host (e.g. gitea.com); overrides FORGE_HOST and remote detection") 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)") } diff --git a/internal/resolve/resolve.go b/internal/resolve/resolve.go index 4366920..b7d89f1 100644 --- a/internal/resolve/resolve.go +++ b/internal/resolve/resolve.go @@ -15,7 +15,10 @@ import ( "github.com/git-pkgs/forge/internal/config" ) -var remoteName = "origin" +var ( + remoteName = "origin" + hostOverride string +) // SetRemote sets which git remote to read when resolving the current // repository. The CLI calls this from the --remote persistent flag. @@ -27,6 +30,15 @@ func SetRemote(name string) { } } +// SetHost forces a specific forge domain, taking precedence over FORGE_HOST, +// --forge-type, and git remote detection. The CLI calls this from the --host +// persistent flag. An empty string is ignored. +func SetHost(host string) { + if host != "" { + hostOverride = host + } +} + var builders = forges.ForgeBuilders{ GitHub: ghforge.NewWithBase, GitLab: glforge.New, @@ -50,7 +62,7 @@ func repoFromFlag(flagRepo, flagForgeType string) (forges.Forge, string, string, } owner, repo := flagRepo[:lastSlash], flagRepo[lastSlash+1:] - domain := DomainFromForgeType(flagForgeType) + domain := Domain(flagForgeType) client := newClient(domain) f, err := forgeForDomainMaybeConfig(context.Background(), client, domain) if err != nil { @@ -221,21 +233,30 @@ func ForgeForDomain(domain string) (forges.Forge, error) { return forgeForDomainMaybeConfig(context.Background(), client, domain) } -// DomainFromForgeType returns the default domain for a forge type string. -// Checks FORGE_HOST first, then config default, then well-known defaults. -func DomainFromForgeType(forgeType string) string { +// Domain decides which forge host to talk to when the user supplies a bare +// owner or owner/repo argument. Precedence: --host flag, FORGE_HOST env, +// explicit --forge-type, the current directory's git remote, the config +// default forge type, then github.com. +func Domain(forgeType string) string { + if hostOverride != "" { + return hostOverride + } if d := os.Getenv("FORGE_HOST"); d != "" { return d } - - // If no forge type given, check config for a default - if forgeType == "" { - cfg, err := config.Load() - if err == nil && cfg != nil && cfg.Default.ForgeType != "" { - forgeType = cfg.Default.ForgeType - } + if forgeType != "" { + return defaultDomainForType(forgeType) + } + if d, _, _, err := resolveRemote(); err == nil { + return d } + if cfg, err := config.Load(); err == nil && cfg != nil && cfg.Default.ForgeType != "" { + return defaultDomainForType(cfg.Default.ForgeType) + } + return "github.com" +} +func defaultDomainForType(forgeType string) string { switch forgeType { case "gitlab": return "gitlab.com" diff --git a/internal/resolve/resolve_test.go b/internal/resolve/resolve_test.go index 37f4233..85f9b5d 100644 --- a/internal/resolve/resolve_test.go +++ b/internal/resolve/resolve_test.go @@ -122,7 +122,9 @@ func TestTokenForDomainEnvFallbackForUnknownDomain(t *testing.T) { } } -func TestDomainFromForgeType(t *testing.T) { +func TestDomain(t *testing.T) { + t.Chdir(t.TempDir()) + tests := []struct { forgeType string want string @@ -137,27 +139,93 @@ func TestDomainFromForgeType(t *testing.T) { } for _, tt := range tests { - got := DomainFromForgeType(tt.forgeType) + got := Domain(tt.forgeType) if got != tt.want { - t.Errorf("DomainFromForgeType(%q) = %q, want %q", tt.forgeType, got, tt.want) + t.Errorf("Domain(%q) = %q, want %q", tt.forgeType, got, tt.want) } } } -func TestDomainFromForgeTypeWithForgeHost(t *testing.T) { +func TestDomainWithForgeHost(t *testing.T) { + t.Chdir(t.TempDir()) t.Setenv("FORGE_HOST", "git.example.com") - got := DomainFromForgeType("github") + got := Domain("github") if got != "git.example.com" { t.Errorf("expected FORGE_HOST override, got %q", got) } - got = DomainFromForgeType("") + got = Domain("") if got != "git.example.com" { t.Errorf("expected FORGE_HOST override for empty type, got %q", got) } } +func TestDomainFallsBackToGitRemote(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not installed") + } + + t.Chdir(t.TempDir()) + mustGit(t, "init", "-q") + mustGit(t, "remote", "add", "origin", "https://gitea.com/someone/project.git") + + got := Domain("") + if got != "gitea.com" { + t.Errorf("expected domain from git remote, got %q", got) + } +} + +func TestDomainExplicitForgeTypeOverridesRemote(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not installed") + } + + t.Chdir(t.TempDir()) + mustGit(t, "init", "-q") + mustGit(t, "remote", "add", "origin", "https://gitea.com/someone/project.git") + + got := Domain("gitlab") + if got != "gitlab.com" { + t.Errorf("expected --forge-type to override remote, got %q", got) + } +} + +func TestDomainHostOverride(t *testing.T) { + if _, err := exec.LookPath("git"); err != nil { + t.Skip("git not installed") + } + + t.Chdir(t.TempDir()) + mustGit(t, "init", "-q") + mustGit(t, "remote", "add", "origin", "https://github.com/someone/project.git") + t.Setenv("FORGE_HOST", "env.example.com") + + old := hostOverride + defer func() { hostOverride = old }() + SetHost("flag.example.com") + + got := Domain("gitlab") + if got != "flag.example.com" { + t.Errorf("expected --host to override everything, got %q", got) + } +} + +func TestSetHost(t *testing.T) { + old := hostOverride + defer func() { hostOverride = old }() + + SetHost("gitea.com") + if hostOverride != "gitea.com" { + t.Errorf("SetHost did not update hostOverride, got %q", hostOverride) + } + + SetHost("") + if hostOverride != "gitea.com" { + t.Errorf("SetHost(\"\") should be a no-op, got %q", hostOverride) + } +} + func TestRemoteDefaultsToOrigin(t *testing.T) { if remoteName != "origin" { t.Errorf("default remote should be origin, got %q", remoteName)