fix(api): 单次调用支持 :owner/:repo 与 {{owner}}/{{repo}} 占位符替换(issue #20)

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
maidamaliziasimnw 2026-07-10 16:45:06 +00:00
parent 604303e953
commit a383b0a00b
2 changed files with 76 additions and 0 deletions

View File

@ -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")

View File

@ -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 (<METHOD> <PATH> | --batch-file <FILE>)" {