Merge pull request #406: fix(api): 修复 Windows Git Bash 下 API 路径被 MSYS2 污染导致 404
# Conflicts: # cmd/api/api.go
This commit is contained in:
commit
7d706f4e4c
|
|
@ -14,19 +14,10 @@ import (
|
|||
|
||||
"github.com/gitlink-org/gitlink-cli/cmd/cmdutil"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/context"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||||
)
|
||||
|
||||
// apiOwnerPlaceholder and apiRepoPlaceholder match the REST-style :owner / :repo
|
||||
// path placeholders used throughout the GitLink API docs and shortcut commands.
|
||||
// The \b boundary keeps :owner/:repo from matching longer tokens like :owner_id.
|
||||
var (
|
||||
apiOwnerPlaceholder = regexp.MustCompile(`:owner\b`)
|
||||
apiRepoPlaceholder = regexp.MustCompile(`:repo\b`)
|
||||
)
|
||||
|
||||
func NewAPICmd(translators ...*i18n.Translator) *cobra.Command {
|
||||
tr := i18n.Default()
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
|
|
@ -40,8 +31,6 @@ func NewAPICmd(translators ...*i18n.Translator) *cobra.Command {
|
|||
gitlink-cli api GET /projects --query 'page=1&limit=10'
|
||||
gitlink-cli api POST /:owner/:repo/issues --body '{"subject":"Bug","description":"..."}'
|
||||
gitlink-cli api POST /:owner/:repo/issues --body-file issue.json
|
||||
gitlink-cli api GET /:owner/:repo/commits --owner Gitlink --repo gitlink-cli
|
||||
gitlink-cli api GET /v1/{{owner}}/gitlink-cli/issues --var owner=Gitlink
|
||||
gitlink-cli api --batch-file plan.json --dry-run
|
||||
gitlink-cli api --batch-file plan.json --var owner=Gitlink --var repo=gitlink-cli`,
|
||||
Args: validateAPIArgs,
|
||||
|
|
@ -72,6 +61,32 @@ func validateAPIArgs(c *cobra.Command, args []string) error {
|
|||
return cobra.ExactArgs(2)(c, args)
|
||||
}
|
||||
|
||||
// msysPathRe matches Windows drive-letter prefixes produced by MSYS2/Git Bash
|
||||
// path conversion, e.g. "C:/Program Files/Git/v1/owner/repo" for input "/v1/owner/repo".
|
||||
var msysPathRe = regexp.MustCompile(`^[A-Za-z]:/`)
|
||||
|
||||
// restoreAPIPath restores an API path polluted by MSYS2/Git Bash path
|
||||
// conversion on Windows, e.g. "C:/Program Files/Git/v1/owner/repo" -> "/v1/owner/repo".
|
||||
// If the path does not start with a drive letter, or no known API prefix is
|
||||
// found, the original path is returned unchanged.
|
||||
func restoreAPIPath(path string) string {
|
||||
if !msysPathRe.MatchString(path) {
|
||||
return path
|
||||
}
|
||||
// Pick the EARLIEST occurrence among known API prefixes, so a path like
|
||||
// ".../api/v1/users" restores to "/api/v1/users" rather than "/v1/users".
|
||||
bestIdx := -1
|
||||
for _, prefix := range []string{"/v1/", "/v2/", "/api/", "/users/", "/projects/"} {
|
||||
if idx := strings.Index(path, prefix); idx >= 0 && (bestIdx == -1 || idx < bestIdx) {
|
||||
bestIdx = idx
|
||||
}
|
||||
}
|
||||
if bestIdx >= 0 {
|
||||
return path[bestIdx:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func runAPI(c *cobra.Command, args []string) error {
|
||||
batchFile, _ := c.Flags().GetString("batch-file")
|
||||
if batchFile != "" {
|
||||
|
|
@ -79,9 +94,14 @@ func runAPI(c *cobra.Command, args []string) error {
|
|||
}
|
||||
|
||||
method := strings.ToUpper(args[0])
|
||||
path, err := resolveAPIPath(c, args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
path := args[1]
|
||||
|
||||
// Fix MSYS2/Git Bash path auto-conversion on Windows:
|
||||
// "/v1/owner/repo" is rewritten to "C:/Program Files/Git/v1/owner/repo".
|
||||
path = restoreAPIPath(path)
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
|
||||
cli, err := client.New()
|
||||
|
|
@ -118,42 +138,6 @@ func runAPI(c *cobra.Command, args []string) error {
|
|||
return output.Print(env, resolveFormat())
|
||||
}
|
||||
|
||||
// resolveAPIPath prepares a single-call path: it renders {{var}} templates
|
||||
// supplied via --var (consistent with batch mode), substitutes the REST-style
|
||||
// :owner / :repo placeholders (resolved from --owner/--repo or the git remote,
|
||||
// exactly like the shortcut commands), and ensures a leading slash.
|
||||
func resolveAPIPath(c *cobra.Command, rawPath string) (string, error) {
|
||||
path := rawPath
|
||||
|
||||
overrides, err := parseBatchVars(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(overrides) > 0 {
|
||||
rendered, rerr := renderTemplate(path, overrides)
|
||||
if rerr != nil {
|
||||
return "", rerr
|
||||
}
|
||||
path = rendered
|
||||
}
|
||||
|
||||
if apiOwnerPlaceholder.MatchString(path) || apiRepoPlaceholder.MatchString(path) {
|
||||
owner, repo, rerr := context.ResolveOwnerRepo(cmdutil.Owner, cmdutil.Repo)
|
||||
if rerr != nil {
|
||||
return "", fmt.Errorf("path contains :owner/:repo placeholders but they could not be resolved: %w", rerr)
|
||||
}
|
||||
// ReplaceAllLiteralString avoids interpreting $ in owner/repo as a
|
||||
// regexp replacement reference.
|
||||
path = apiOwnerPlaceholder.ReplaceAllLiteralString(path, owner)
|
||||
path = apiRepoPlaceholder.ReplaceAllLiteralString(path, repo)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func readJSONBody(c *cobra.Command) (interface{}, error) {
|
||||
bodyStr, _ := c.Flags().GetString("body")
|
||||
bodyFile, _ := c.Flags().GetString("body-file")
|
||||
|
|
|
|||
|
|
@ -452,3 +452,25 @@ func writeBatchPlan(t *testing.T, payload interface{}) string {
|
|||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestRestoreAPIPath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
want string
|
||||
}{
|
||||
{"normal v1 path unchanged", "/v1/owner/repo", "/v1/owner/repo"},
|
||||
{"msys2 polluted v1", "C:/Program Files/Git/v1/owner/repo", "/v1/owner/repo"},
|
||||
{"msys2 polluted v2", "D:/Git/v2/x/y", "/v2/x/y"},
|
||||
{"msys2 polluted api prefix", "C:/Program Files/Git/api/v1/users", "/api/v1/users"},
|
||||
{"drive letter but no known prefix", "C:/something/else", "C:/something/else"},
|
||||
{"empty path", "", ""},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := restoreAPIPath(tt.path); got != tt.want {
|
||||
t.Fatalf("restoreAPIPath(%q) = %q, want %q", tt.path, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
# Fix: Windows Git Bash 下 `gitlink-cli api` 路径被 MSYS2 污染导致 404
|
||||
|
||||
## 问题
|
||||
|
||||
在 Windows Git Bash(MSYS2)环境下执行:
|
||||
|
||||
```bash
|
||||
gitlink-cli api GET /v1/owner/repo
|
||||
```
|
||||
|
||||
路径参数 `/v1/owner/repo` 会被 MSYS2 自动改写为类似 `C:/Program Files/Git/v1/owner/repo` 的 Windows 路径——MSYS2 把以 `/` 开头的命令行参数当成 Unix 路径,转换为 Git 安装目录。结果 API 请求路径错误,返回 404。影响所有 Windows Git Bash 用户。
|
||||
|
||||
## 根因
|
||||
|
||||
MSYS2 的 POSIX→Windows 路径转换会对命令行参数中以 `/` 开头的字符串生效,且无法通过 shell 转义稳定规避(`MSYS_NO_PATHCONV` 等环境变量依赖用户配置,不可靠)。
|
||||
|
||||
## 修复
|
||||
|
||||
在 `cmd/api` 的 `runAPI` 中,对取到的 `path` 调用 `restoreAPIPath` 还原:
|
||||
|
||||
- 检测首部是否为盘符(正则 `^[A-Za-z]:/`);
|
||||
- 若是,按常见 API 前缀(`/v1/` `/v2/` `/api/` `/users/` `/projects/`)在污染后的路径里定位原始起点并截取;
|
||||
- 无盘符或无匹配前缀时原样返回,不影响其他平台与正常路径。
|
||||
|
||||
`restoreAPIPath` 为纯函数,便于单元测试。
|
||||
|
||||
## 影响
|
||||
|
||||
仅 Windows 受益,其他平台行为不变。改动集中在 `cmd/api/api.go`(约 +25 行,含函数与注释)。
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
go test ./cmd/api/... -run TestRestoreAPIPath -v
|
||||
```
|
||||
Loading…
Reference in New Issue