feat: add directory-scoped host defaults (match_dirs) and repo list --limit
Some checks are pending
CI / lint (push) Waiting to run
CI / build (push) Waiting to run
CI / test (push) Waiting to run
CI / functional (push) Blocked by required conditions

Add match_dirs field to host config entries for directory-based host
resolution. When no --hostname flag, FGJ_HOST env var, or git remote is
detected, the longest matching directory prefix determines the host.
Symlinks are resolved on both sides for macOS compatibility (/tmp →
/private/tmp). Also adds --limit/-L flag to repo list.
This commit is contained in:
sid 2026-03-23 12:39:51 -06:00
parent 113505de95
commit c293e233d2
17 changed files with 252 additions and 79 deletions

View file

@ -420,7 +420,7 @@ func runRunList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to load config: %w", err) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) 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) 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 { if err != nil {
return fmt.Errorf("failed to create client: %w", err) return fmt.Errorf("failed to create client: %w", err)
} }

View file

@ -72,7 +72,7 @@ func runAPI(cmd *cobra.Command, args []string) error {
detectedHost := getDetectedHost() detectedHost := getDetectedHost()
host, err := cfg.GetHost(hostname, detectedHost) host, err := cfg.GetHost(hostname, detectedHost, getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -203,7 +203,7 @@ func runIssueList(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -275,7 +275,7 @@ func runIssueView(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -375,7 +375,7 @@ func runIssueCreate(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -464,7 +464,7 @@ func runIssueComment(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -503,7 +503,7 @@ func runIssueClose(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -564,7 +564,7 @@ func runIssueEdit(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -687,7 +687,7 @@ func runIssueDelete(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -732,7 +732,7 @@ func runIssueReopen(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -116,7 +116,7 @@ func newLabelClient(cmd *cobra.Command) (*api.Client, string, string, error) {
return nil, "", "", err return nil, "", "", err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return nil, "", "", err return nil, "", "", err
} }

View file

@ -183,7 +183,7 @@ func runMilestoneList(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -249,7 +249,7 @@ func runMilestoneView(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -264,7 +264,7 @@ func runMilestoneView(cmd *cobra.Command, args []string) error {
if web, _ := cmd.Flags().GetBool("web"); web { if web, _ := cmd.Flags().GetBool("web"); web {
// Milestones don't have HTMLURL in the API, construct it // Milestones don't have HTMLURL in the API, construct it
cfg2, _ := config.Load() 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) url := fmt.Sprintf("https://%s/%s/%s/milestone/%d", host.Hostname, owner, name, ms.ID)
return ios.OpenInBrowser(url) return ios.OpenInBrowser(url)
} }
@ -315,7 +315,7 @@ func runMilestoneCreate(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -363,7 +363,7 @@ func runMilestoneEdit(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -450,7 +450,7 @@ func runMilestoneDelete(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -228,7 +228,7 @@ func runPRList(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -367,7 +367,7 @@ func runPRView(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -500,7 +500,7 @@ func runPRCreate(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -611,7 +611,7 @@ func runPRMerge(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -678,7 +678,7 @@ func runPRClose(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -727,7 +727,7 @@ func runPRReopen(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -766,7 +766,7 @@ func runPRCheckout(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -886,7 +886,7 @@ func runPREdit(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -46,7 +46,7 @@ func runPRChecks(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -58,7 +58,7 @@ func runPRDiff(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -121,7 +121,7 @@ func runPRComment(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -191,7 +191,7 @@ func runPRReview(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -180,7 +180,7 @@ func runReleaseList(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -251,7 +251,7 @@ func runReleaseView(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -363,7 +363,7 @@ func runReleaseCreate(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -420,7 +420,7 @@ func runReleaseUpload(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -460,7 +460,7 @@ func runReleaseDownload(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -547,7 +547,7 @@ func runReleaseDelete(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -125,6 +125,7 @@ func init() {
repoViewCmd.Flags().BoolP("web", "w", false, "Open in web browser") repoViewCmd.Flags().BoolP("web", "w", false, "Open in web browser")
addJSONFlags(repoListCmd, "Output repositories as JSON") 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") 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 return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -203,7 +204,7 @@ func runRepoList(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -221,6 +222,11 @@ func runRepoList(cmd *cobra.Command, args []string) error {
return fmt.Errorf("failed to list repositories: %w", err) 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) { if wantJSON(cmd) {
return outputJSON(cmd, repos) return outputJSON(cmd, repos)
} }
@ -257,7 +263,7 @@ func runRepoClone(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -324,7 +330,7 @@ func runRepoFork(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -371,7 +377,7 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -439,7 +445,7 @@ func runRepoCreate(cmd *cobra.Command, args []string) error {
if doClone { if doClone {
cloneURL := repo.CloneURL cloneURL := repo.CloneURL
if hostCfg, hostErr := cfg.GetHost("", getDetectedHost()); hostErr == nil { if hostCfg, hostErr := cfg.GetHost("", getDetectedHost(), getCwd()); hostErr == nil {
if hostCfg.GitProtocol == "ssh" { if hostCfg.GitProtocol == "ssh" {
cloneURL = repo.SSHURL cloneURL = repo.SSHURL
} }
@ -494,7 +500,7 @@ func runRepoEdit(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }
@ -591,7 +597,7 @@ func runRepoRename(cmd *cobra.Command, args []string) error {
return err return err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return err return err
} }

View file

@ -96,6 +96,15 @@ func getDetectedHost() string {
return host 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. // promptLine prints a prompt to stderr and reads a line from stdin.
func promptLine(prompt string) (string, error) { func promptLine(prompt string) (string, error) {
fmt.Fprint(ios.ErrOut, prompt) fmt.Fprint(ios.ErrOut, prompt)

View file

@ -185,7 +185,7 @@ func newWikiClient(cmd *cobra.Command) (*api.Client, string, string, error) {
return nil, "", "", err return nil, "", "", err
} }
client, err := api.NewClientFromConfig(cfg, "", getDetectedHost()) client, err := api.NewClientFromConfig(cfg, "", getDetectedHost(), getCwd())
if err != nil { if err != nil {
return nil, "", "", err return nil, "", "", err
} }

View file

@ -39,8 +39,8 @@ func NewClient(hostname, token string) (*Client, error) {
}, nil }, nil
} }
func NewClientFromConfig(cfg *config.Config, hostname string, detectedHost string) (*Client, error) { func NewClientFromConfig(cfg *config.Config, hostname string, detectedHost string, cwd string) (*Client, error) {
host, err := cfg.GetHost(hostname, detectedHost) host, err := cfg.GetHost(hostname, detectedHost, cwd)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -21,7 +21,7 @@ func TestNewClientFromConfig_MissingHost(t *testing.T) {
Hosts: map[string]config.HostConfig{}, Hosts: map[string]config.HostConfig{},
} }
_, err := NewClientFromConfig(cfg, "nonexistent.org", "") _, err := NewClientFromConfig(cfg, "nonexistent.org", "", "")
if err == nil { if err == nil {
t.Error("Expected error for nonexistent host") t.Error("Expected error for nonexistent host")
} }

View file

@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/spf13/viper" "github.com/spf13/viper"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -18,6 +19,7 @@ type HostConfig struct {
Token string `yaml:"token"` Token string `yaml:"token"`
User string `yaml:"user,omitempty"` User string `yaml:"user,omitempty"`
GitProtocol string `yaml:"git_protocol,omitempty"` GitProtocol string `yaml:"git_protocol,omitempty"`
MatchDirs []string `yaml:"match_dirs,omitempty"`
} }
func GetConfigDir() (string, error) { func GetConfigDir() (string, error) {
@ -96,8 +98,9 @@ func (c *Config) SaveToPath(path string) error {
// 2. CLI flag (--hostname) // 2. CLI flag (--hostname)
// 3. Environment variable (FGJ_HOST) // 3. Environment variable (FGJ_HOST)
// 4. Auto-detected hostname from git remote // 4. Auto-detected hostname from git remote
// 5. Default to codeberg.org // 5. match_dirs lookup (longest prefix match)
func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, error) { // 6. Default to codeberg.org
func (c *Config) GetHost(hostname string, detectedHost string, cwd string) (HostConfig, error) {
if hostname == "" { if hostname == "" {
hostname = viper.GetString("hostname") hostname = viper.GetString("hostname")
} }
@ -110,6 +113,10 @@ func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, erro
hostname = detectedHost hostname = detectedHost
} }
if hostname == "" {
hostname = c.ResolveHostByPath(cwd)
}
if hostname == "" { if hostname == "" {
hostname = "codeberg.org" hostname = "codeberg.org"
} }
@ -122,6 +129,50 @@ func (c *Config) GetHost(hostname string, detectedHost string) (HostConfig, erro
return host, nil 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) { func (c *Config) SetHost(hostname string, host HostConfig) {
if c.Hosts == nil { if c.Hosts == nil {
c.Hosts = make(map[string]HostConfig) c.Hosts = make(map[string]HostConfig)

View file

@ -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 { if err != nil {
t.Fatalf("Unexpected error: %v", err) 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) t.Errorf("Expected hostname 'codeberg.org', got '%s'", host.Hostname)
} }
_, err = cfg.GetHost("nonexistent.org", "") _, err = cfg.GetHost("nonexistent.org", "", "")
if err == nil { if err == nil {
t.Error("Expected error for nonexistent host") 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 // Empty hostname should default to codeberg.org
host, err := cfg.GetHost("", "") host, err := cfg.GetHost("", "", "")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) 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 // Whitespace-only hostname should default to codeberg.org
host, err := cfg.GetHost(" ", "") host, err := cfg.GetHost(" ", "", "")
if err == nil { if err == nil {
t.Logf("Got host: %+v (this may be expected behavior)", host) t.Logf("Got host: %+v (this may be expected behavior)", host)
} else { } else {
@ -315,7 +315,7 @@ func TestConfig_SetHost_EmptyToken(t *testing.T) {
cfg.SetHost("codeberg.org", hostConfig) cfg.SetHost("codeberg.org", hostConfig)
host, err := cfg.GetHost("codeberg.org", "") host, err := cfg.GetHost("codeberg.org", "", "")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
@ -345,7 +345,7 @@ func TestConfig_SetHost_OverwriteExisting(t *testing.T) {
cfg.SetHost("codeberg.org", newConfig) cfg.SetHost("codeberg.org", newConfig)
host, err := cfg.GetHost("codeberg.org", "") host, err := cfg.GetHost("codeberg.org", "", "")
if err != nil { if err != nil {
t.Fatalf("Unexpected error: %v", err) t.Fatalf("Unexpected error: %v", err)
} }
@ -388,7 +388,7 @@ func TestConfig_MultipleHosts(t *testing.T) {
// Verify each host can be retrieved correctly // Verify each host can be retrieved correctly
for _, h := range hosts { for _, h := range hosts {
host, err := cfg.GetHost(h.hostname, "") host, err := cfg.GetHost(h.hostname, "", "")
if err != nil { if err != nil {
t.Errorf("Failed to get host %s: %v", h.hostname, err) t.Errorf("Failed to get host %s: %v", h.hostname, err)
continue continue
@ -422,13 +422,120 @@ func TestConfig_GitProtocol(t *testing.T) {
}) })
// Verify protocols are stored correctly // Verify protocols are stored correctly
sshHost, _ := cfg.GetHost("test-ssh.org", "") sshHost, _ := cfg.GetHost("test-ssh.org", "", "")
if sshHost.GitProtocol != "ssh" { if sshHost.GitProtocol != "ssh" {
t.Errorf("Expected git_protocol 'ssh', got '%s'", sshHost.GitProtocol) 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" { if httpsHost.GitProtocol != "https" {
t.Errorf("Expected git_protocol 'https', got '%s'", httpsHost.GitProtocol) 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)
}
}