diff --git a/cmd/api/api.go b/cmd/api/api.go index cae531a..1e01309 100644 --- a/cmd/api/api.go +++ b/cmd/api/api.go @@ -13,6 +13,7 @@ import ( "github.com/gitlink-org/gitlink-cli/cmd/cmdutil" "github.com/gitlink-org/gitlink-cli/internal/client" + repocontext "github.com/gitlink-org/gitlink-cli/internal/context" "github.com/gitlink-org/gitlink-cli/internal/i18n" "github.com/gitlink-org/gitlink-cli/internal/output" ) @@ -73,6 +74,11 @@ func runAPI(c *cobra.Command, args []string) error { path = "/" + path } + path, err := resolvePathPlaceholders(path) + if err != nil { + return err + } + cli, err := client.New() if err != nil { return err @@ -107,6 +113,27 @@ func runAPI(c *cobra.Command, args []string) error { return output.Print(env, resolveFormat()) } +// resolvePathPlaceholders substitutes :owner/:repo (and {{owner}}/{{repo}}) +// segments in a single-call path with the global --owner/--repo flags or the +// values auto-resolved from the current git remote, matching the help-text +// examples. Paths without placeholders are returned unchanged. +func resolvePathPlaceholders(path string) (string, error) { + hasColon := strings.Contains(path, "/:owner") || strings.Contains(path, "/:repo") + hasBrace := strings.Contains(path, "{{owner}}") || strings.Contains(path, "{{repo}}") + if !hasColon && !hasBrace { + return path, nil + } + owner, repo, err := repocontext.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo) + if err != nil { + return "", fmt.Errorf("path contains :owner/:repo placeholders: %w", err) + } + path = strings.ReplaceAll(path, "/:owner", "/"+owner) + path = strings.ReplaceAll(path, "/:repo", "/"+repo) + path = strings.ReplaceAll(path, "{{owner}}", owner) + path = strings.ReplaceAll(path, "{{repo}}", repo) + return path, nil +} + func readJSONBody(c *cobra.Command) (interface{}, error) { bodyStr, _ := c.Flags().GetString("body") bodyFile, _ := c.Flags().GetString("body-file") diff --git a/cmd/api/api_test.go b/cmd/api/api_test.go index dad41a8..2060267 100644 --- a/cmd/api/api_test.go +++ b/cmd/api/api_test.go @@ -32,6 +32,55 @@ func TestResolveFormat(t *testing.T) { } } +func TestResolvePathPlaceholders(t *testing.T) { + origOwner, origRepo := cmdutil.Owner, cmdutil.Repo + t.Cleanup(func() { cmdutil.Owner, cmdutil.Repo = origOwner, origRepo }) + cmdutil.Owner, cmdutil.Repo = "demo-owner", "demo-repo" + + tests := []struct { + name string + path string + want string + }{ + {"colon placeholders", "/:owner/:repo/issues", "/demo-owner/demo-repo/issues"}, + {"colon with suffix", "/:owner/:repo/issues/42", "/demo-owner/demo-repo/issues/42"}, + {"brace placeholders", "/{{owner}}/{{repo}}/pulls", "/demo-owner/demo-repo/pulls"}, + {"no placeholders unchanged", "/users/me", "/users/me"}, + {"literal path unchanged", "/Gitlink/gitlink-cli/issues", "/Gitlink/gitlink-cli/issues"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := resolvePathPlaceholders(tt.path) + if err != nil { + t.Fatalf("resolvePathPlaceholders(%q): %v", tt.path, err) + } + if got != tt.want { + t.Fatalf("resolvePathPlaceholders(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} + +func TestResolvePathPlaceholdersUnresolvable(t *testing.T) { + origOwner, origRepo := cmdutil.Owner, cmdutil.Repo + t.Cleanup(func() { cmdutil.Owner, cmdutil.Repo = origOwner, origRepo }) + cmdutil.Owner, cmdutil.Repo = "", "" + + tmp := t.TempDir() + origWD, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = os.Chdir(origWD) }) + if err := os.Chdir(tmp); err != nil { + t.Fatal(err) + } + + if _, err := resolvePathPlaceholders("/:owner/:repo/issues"); err == nil { + t.Fatal("expected error when owner/repo cannot be resolved") + } +} + func TestNewAPICmd(t *testing.T) { cmd := NewAPICmd() if cmd.Use != "api ( | --batch-file )" {