diff --git a/cmd/actions.go b/cmd/actions.go index fbb0d75..dc1ebe5 100644 --- a/cmd/actions.go +++ b/cmd/actions.go @@ -420,7 +420,7 @@ func runRunList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -469,7 +469,7 @@ func runRunView(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -668,7 +668,7 @@ func runRunWatch(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -718,7 +718,7 @@ func runRunRerun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -749,7 +749,7 @@ func runRunCancel(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -866,7 +866,7 @@ func runWorkflowList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -939,7 +939,7 @@ func runWorkflowView(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1000,7 +1000,7 @@ func runWorkflowRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1077,7 +1077,7 @@ func runWorkflowEnable(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1122,7 +1122,7 @@ func runWorkflowDisable(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1206,7 +1206,7 @@ func runActionsSecretList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1241,7 +1241,7 @@ func runActionsSecretCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1282,7 +1282,7 @@ func runActionsSecretDelete(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1318,7 +1318,7 @@ func runActionsVariableList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1359,7 +1359,7 @@ func runActionsVariableGet(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1387,7 +1387,7 @@ func runActionsVariableCreate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1416,7 +1416,7 @@ func runActionsVariableUpdate(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } @@ -1445,7 +1445,7 @@ func runActionsVariableDelete(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to load config: %w", err) } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return fmt.Errorf("failed to create client: %w", err) } diff --git a/cmd/api.go b/cmd/api.go index 873f16b..758efdd 100644 --- a/cmd/api.go +++ b/cmd/api.go @@ -72,7 +72,7 @@ func runAPI(cmd *cobra.Command, args []string) error { detectedHost := getDetectedHost() - host, err := cfg.GetHost(hostname, detectedHost) + host, err := cfg.GetHost(hostname, detectedHost, getCwd()) if err != nil { return err } diff --git a/cmd/issue.go b/cmd/issue.go index df752d4..c5d80b8 100644 --- a/cmd/issue.go +++ b/cmd/issue.go @@ -203,7 +203,7 @@ func runIssueList(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -275,7 +275,7 @@ func runIssueView(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -375,7 +375,7 @@ func runIssueCreate(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -464,7 +464,7 @@ func runIssueComment(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -503,7 +503,7 @@ func runIssueClose(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -564,7 +564,7 @@ func runIssueEdit(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -687,7 +687,7 @@ func runIssueDelete(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -732,7 +732,7 @@ func runIssueReopen(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/label.go b/cmd/label.go index 83966d8..4361414 100644 --- a/cmd/label.go +++ b/cmd/label.go @@ -116,7 +116,7 @@ func newLabelClient(cmd *cobra.Command) (*api.Client, string, string, error) { return nil, "", "", err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return nil, "", "", err } diff --git a/cmd/milestone.go b/cmd/milestone.go index 2674a3f..2c38835 100644 --- a/cmd/milestone.go +++ b/cmd/milestone.go @@ -183,7 +183,7 @@ func runMilestoneList(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -249,7 +249,7 @@ func runMilestoneView(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -264,7 +264,7 @@ func runMilestoneView(cmd *cobra.Command, args []string) error { if web, _ := cmd.Flags().GetBool("web"); web { // Milestones don't have HTMLURL in the API, construct it cfg2, _ := config.Load() - host, _ := cfg2.GetHost("", getDetectedHost()) + host, _ := cfg2.GetHost("", getDetectedHost(), getCwd()) url := fmt.Sprintf("https://%s/%s/%s/milestone/%d", host.Hostname, owner, name, ms.ID) return ios.OpenInBrowser(url) } @@ -315,7 +315,7 @@ func runMilestoneCreate(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -363,7 +363,7 @@ func runMilestoneEdit(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -450,7 +450,7 @@ func runMilestoneDelete(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/pr.go b/cmd/pr.go index 72bbb9f..42d3d6f 100644 --- a/cmd/pr.go +++ b/cmd/pr.go @@ -228,7 +228,7 @@ func runPRList(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -367,7 +367,7 @@ func runPRView(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -500,7 +500,7 @@ func runPRCreate(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -611,7 +611,7 @@ func runPRMerge(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -678,7 +678,7 @@ func runPRClose(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -727,7 +727,7 @@ func runPRReopen(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -766,7 +766,7 @@ func runPRCheckout(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -886,7 +886,7 @@ func runPREdit(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/pr_checks.go b/cmd/pr_checks.go index 4198668..f781f95 100644 --- a/cmd/pr_checks.go +++ b/cmd/pr_checks.go @@ -46,7 +46,7 @@ func runPRChecks(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/pr_diff.go b/cmd/pr_diff.go index cee3fce..f91add4 100644 --- a/cmd/pr_diff.go +++ b/cmd/pr_diff.go @@ -58,7 +58,7 @@ func runPRDiff(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/pr_review.go b/cmd/pr_review.go index ea370bf..ab2c8a7 100644 --- a/cmd/pr_review.go +++ b/cmd/pr_review.go @@ -121,7 +121,7 @@ func runPRComment(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -191,7 +191,7 @@ func runPRReview(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/release.go b/cmd/release.go index 7f3ffd4..ba7c29d 100644 --- a/cmd/release.go +++ b/cmd/release.go @@ -180,7 +180,7 @@ func runReleaseList(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -251,7 +251,7 @@ func runReleaseView(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -363,7 +363,7 @@ func runReleaseCreate(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -420,7 +420,7 @@ func runReleaseUpload(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -460,7 +460,7 @@ func runReleaseDownload(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -547,7 +547,7 @@ func runReleaseDelete(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/repo.go b/cmd/repo.go index d216198..620d942 100644 --- a/cmd/repo.go +++ b/cmd/repo.go @@ -125,6 +125,7 @@ func init() { repoViewCmd.Flags().BoolP("web", "w", false, "Open in web browser") addJSONFlags(repoListCmd, "Output repositories as JSON") + repoListCmd.Flags().IntP("limit", "L", 0, "Maximum number of repositories to list (0 = no limit)") repoCloneCmd.Flags().StringP("protocol", "p", "https", "Clone protocol: https or ssh") @@ -158,7 +159,7 @@ func runRepoView(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -203,7 +204,7 @@ func runRepoList(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -221,6 +222,11 @@ func runRepoList(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to list repositories: %w", err) } + limit, _ := cmd.Flags().GetInt("limit") + if limit > 0 && len(repos) > limit { + repos = repos[:limit] + } + if wantJSON(cmd) { return outputJSON(cmd, repos) } @@ -257,7 +263,7 @@ func runRepoClone(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -324,7 +330,7 @@ func runRepoFork(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -371,7 +377,7 @@ func runRepoCreate(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -439,7 +445,7 @@ func runRepoCreate(cmd *cobra.Command, args []string) error { if doClone { cloneURL := repo.CloneURL - if hostCfg, hostErr := cfg.GetHost("", getDetectedHost()); hostErr == nil { + if hostCfg, hostErr := cfg.GetHost("", getDetectedHost(), getCwd()); hostErr == nil { if hostCfg.GitProtocol == "ssh" { cloneURL = repo.SSHURL } @@ -494,7 +500,7 @@ func runRepoEdit(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } @@ -591,7 +597,7 @@ func runRepoRename(cmd *cobra.Command, args []string) error { return err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return err } diff --git a/cmd/root.go b/cmd/root.go index edb1c39..26b3ff0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -96,6 +96,15 @@ func getDetectedHost() string { return host } +// getCwd returns the current working directory, or "" on error. +func getCwd() string { + cwd, err := os.Getwd() + if err != nil { + return "" + } + return cwd +} + // promptLine prints a prompt to stderr and reads a line from stdin. func promptLine(prompt string) (string, error) { fmt.Fprint(ios.ErrOut, prompt) diff --git a/cmd/wiki.go b/cmd/wiki.go index 4c2d65d..3992c86 100644 --- a/cmd/wiki.go +++ b/cmd/wiki.go @@ -185,7 +185,7 @@ func newWikiClient(cmd *cobra.Command) (*api.Client, string, string, error) { return nil, "", "", err } - client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) + client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd()) if err != nil { return nil, "", "", err } diff --git a/internal/api/client.go b/internal/api/client.go index d5869d3..d91ca36 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -39,8 +39,8 @@ func NewClient(hostname, token string) (*Client, error) { }, nil } -func NewClientFromConfig(cfg *config.Config, hostname string, detectedHost string) (*Client, error) { - host, err := cfg.GetHost(hostname, detectedHost) +func NewClientFromConfig(cfg *config.Config, hostname string, detectedHost string, cwd string) (*Client, error) { + host, err := cfg.GetHost(hostname, detectedHost, cwd) if err != nil { return nil, err } diff --git a/internal/api/client_test.go b/internal/api/client_test.go index daf8b3c..31146e1 100644 --- a/internal/api/client_test.go +++ b/internal/api/client_test.go @@ -21,7 +21,7 @@ func TestNewClientFromConfig_MissingHost(t *testing.T) { Hosts: map[string]config.HostConfig{}, } - _, err := NewClientFromConfig(cfg, "nonexistent.org", "") + _, err := NewClientFromConfig(cfg, "nonexistent.org", "", "") if err == nil { t.Error("Expected error for nonexistent host") } diff --git a/internal/config/config.go b/internal/config/config.go index 2f22243..bc42159 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "path/filepath" + "strings" "github.com/spf13/viper" "gopkg.in/yaml.v3" @@ -14,10 +15,11 @@ type Config struct { } type HostConfig struct { - Hostname string `yaml:"hostname"` - Token string `yaml:"token"` - User string `yaml:"user,omitempty"` - GitProtocol string `yaml:"git_protocol,omitempty"` + Hostname string `yaml:"hostname"` + Token string `yaml:"token"` + User string `yaml:"user,omitempty"` + GitProtocol string `yaml:"git_protocol,omitempty"` + MatchDirs []string `yaml:"match_dirs,omitempty"` } func GetConfigDir() (string, error) { @@ -96,8 +98,9 @@ func (c *Config) SaveToPath(path string) error { // 2. CLI flag (--hostname) // 3. Environment variable (FGJ_HOST) // 4. Auto-detected hostname from git remote -// 5. Default to codeberg.org -func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, error) { +// 5. match_dirs lookup (longest prefix match) +// 6. Default to codeberg.org +func (c *Config) GetHost(hostname string, detectedHost string, cwd string) (HostConfig, error) { if hostname == "" { hostname = viper.GetString("hostname") } @@ -110,6 +113,10 @@ func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, erro hostname = detectedHost } + if hostname == "" { + hostname = c.ResolveHostByPath(cwd) + } + if hostname == "" { hostname = "codeberg.org" } @@ -122,6 +129,50 @@ func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, erro return host, nil } +// ResolveHostByPath finds the host whose match_dirs entry is the longest +// prefix of cwd. Returns "" if no match is found. +// Both cwd and match_dirs entries are resolved through filepath.EvalSymlinks +// to handle symlinks (e.g. macOS /tmp → /private/tmp). +func (c *Config) ResolveHostByPath(cwd string) string { + if cwd == "" { + return "" + } + + // Resolve symlinks in cwd so /tmp becomes /private/tmp on macOS, etc. + if resolved, err := filepath.EvalSymlinks(cwd); err == nil { + cwd = resolved + } + + bestHost := "" + bestLen := 0 + + for hostname, host := range c.Hosts { + for _, dir := range host.MatchDirs { + if dir == "" { + continue + } + // Resolve symlinks in the configured dir as well + if resolved, err := filepath.EvalSymlinks(dir); err == nil { + dir = resolved + } + // Normalize: ensure trailing slash for prefix matching + prefix := dir + if !strings.HasSuffix(prefix, "/") { + prefix += "/" + } + // Match if cwd equals dir exactly or is under it + if cwd == dir || strings.HasPrefix(cwd, prefix) { + if len(dir) > bestLen { + bestLen = len(dir) + bestHost = hostname + } + } + } + } + + return bestHost +} + func (c *Config) SetHost(hostname string, host HostConfig) { if c.Hosts == nil { c.Hosts = make(map[string]HostConfig) diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 3d55ee9..95282f2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -49,7 +49,7 @@ func TestConfig_GetHost(t *testing.T) { }, } - host, err := cfg.GetHost("codeberg.org", "") + host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -58,7 +58,7 @@ func TestConfig_GetHost(t *testing.T) { t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname) } - _, err = cfg.GetHost("nonexistent.org", "") + _, err = cfg.GetHost("nonexistent.org", "", "") if err == nil { t.Error("Expected error for nonexistent host") } @@ -275,7 +275,7 @@ func TestConfig_GetHost_EmptyString(t *testing.T) { } // Empty hostname should default to codeberg.org - host, err := cfg.GetHost("", "") + host, err := cfg.GetHost("", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -296,7 +296,7 @@ func TestConfig_GetHost_WhitespaceString(t *testing.T) { } // Whitespace-only hostname should default to codeberg.org - host, err := cfg.GetHost(" ", "") + host, err := cfg.GetHost(" ", "", "") if err == nil { t.Logf("Got host: %+v (this may be expected behavior)", host) } else { @@ -315,7 +315,7 @@ func TestConfig_SetHost_EmptyToken(t *testing.T) { cfg.SetHost("codeberg.org", hostConfig) - host, err := cfg.GetHost("codeberg.org", "") + host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -345,7 +345,7 @@ func TestConfig_SetHost_OverwriteExisting(t *testing.T) { cfg.SetHost("codeberg.org", newConfig) - host, err := cfg.GetHost("codeberg.org", "") + host, err := cfg.GetHost("codeberg.org", "", "") if err != nil { t.Fatalf("Unexpected error: %v", err) } @@ -388,7 +388,7 @@ func TestConfig_MultipleHosts(t *testing.T) { // Verify each host can be retrieved correctly for _, h := range hosts { - host, err := cfg.GetHost(h.hostname, "") + host, err := cfg.GetHost(h.hostname, "", "") if err != nil { t.Errorf("Failed to get host %s: %v", h.hostname, err) continue @@ -422,13 +422,120 @@ func TestConfig_GitProtocol(t *testing.T) { }) // Verify protocols are stored correctly - sshHost, _ := cfg.GetHost("test-ssh.org", "") + sshHost, _ := cfg.GetHost("test-ssh.org", "", "") if sshHost.GitProtocol != "ssh" { t.Errorf("Expected git_protocol 'ssh', got '%s'", sshHost.GitProtocol) } - httpsHost, _ := cfg.GetHost("test-https.org", "") + httpsHost, _ := cfg.GetHost("test-https.org", "", "") if httpsHost.GitProtocol != "https" { t.Errorf("Expected git_protocol 'https', got '%s'", httpsHost.GitProtocol) } } + +func TestResolveHostByPath(t *testing.T) { + cfg := &Config{ + Hosts: map[string]HostConfig{ + "forgejo.zerova.net": { + Hostname: "forgejo.zerova.net", + Token: "token1", + MatchDirs: []string{"/Users/sid/repos/fgj", "/Users/sid/repos/zerova"}, + }, + "codeberg.org": { + Hostname: "codeberg.org", + Token: "token2", + MatchDirs: []string{"/"}, + }, + "gitea.example.com": { + Hostname: "gitea.example.com", + Token: "token3", + // no match_dirs — should never be selected by path + }, + }, + } + + tests := []struct { + name string + cwd string + want string + }{ + {"exact dir match", "/Users/sid/repos/fgj", "forgejo.zerova.net"}, + {"nested dir match", "/Users/sid/repos/fgj/cmd/root.go", "forgejo.zerova.net"}, + {"second match dir", "/Users/sid/repos/zerova/pkg", "forgejo.zerova.net"}, + {"longest prefix wins over /", "/Users/sid/repos/fgj/internal", "forgejo.zerova.net"}, + {"/ as global catch-all", "/tmp", "codeberg.org"}, + {"/ matches root itself", "/", "codeberg.org"}, + {"no match_dirs host not selected", "/some/random/path", "codeberg.org"}, + {"empty cwd returns empty", "", ""}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := cfg.ResolveHostByPath(tt.cwd) + if got != tt.want { + t.Errorf("ResolveHostByPath(%q) = %q, want %q", tt.cwd, got, tt.want) + } + }) + } +} + +func TestResolveHostByPath_LongestPrefixAcrossHosts(t *testing.T) { + cfg := &Config{ + Hosts: map[string]HostConfig{ + "broad.org": { + Hostname: "broad.org", + Token: "t1", + MatchDirs: []string{"/Users/sid"}, + }, + "specific.org": { + Hostname: "specific.org", + Token: "t2", + MatchDirs: []string{"/Users/sid/repos/myproject"}, + }, + }, + } + + got := cfg.ResolveHostByPath("/Users/sid/repos/myproject/main.go") + if got != "specific.org" { + t.Errorf("expected specific.org, got %q", got) + } + + got = cfg.ResolveHostByPath("/Users/sid/other") + if got != "broad.org" { + t.Errorf("expected broad.org, got %q", got) + } +} + +func TestGetHost_MatchDirsIntegration(t *testing.T) { + cfg := &Config{ + Hosts: map[string]HostConfig{ + "forgejo.zerova.net": { + Hostname: "forgejo.zerova.net", + Token: "token1", + MatchDirs: []string{"/Users/sid/repos/fgj"}, + }, + "codeberg.org": { + Hostname: "codeberg.org", + Token: "token2", + }, + }, + } + + // cwd match should resolve to forgejo.zerova.net + host, err := cfg.GetHost("", "", "/Users/sid/repos/fgj/cmd") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if host.Hostname != "forgejo.zerova.net" { + t.Errorf("expected forgejo.zerova.net, got %s", host.Hostname) + } + + // no cwd match falls through to codeberg.org default + host, err = cfg.GetHost("", "", "/tmp") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if host.Hostname != "codeberg.org" { + t.Errorf("expected codeberg.org, got %s", host.Hostname) + } +}