docs(api): add MSYS2 path fix doc; extract restoreAPIPath for testability

- 新增命令帮助文档 doc/changes/api-msys2-path.md(问题/根因/修复/影响/测试)。
- 将 runAPI 内联的路径还原逻辑抽成纯函数 restoreAPIPath,便于单元测试;
  并修正一个边界缺陷:路径含多个已知前缀时(如 /api/v1/users)改为取
  最早出现的位置,避免误截到 /v1/。
- 新增表驱动 TestRestoreAPIPath 覆盖:正常路径不变、v1/v2/api 污染还原、
  无已知前缀保留、空路径。
This commit is contained in:
chroe 2026-07-09 22:27:52 +08:00
parent b0424aa6d8
commit bf9ecc4ba2
3 changed files with 80 additions and 9 deletions

View File

@ -65,6 +65,28 @@ func validateAPIArgs(c *cobra.Command, args []string) error {
// 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 != "" {
@ -76,15 +98,7 @@ func runAPI(c *cobra.Command, args []string) error {
// Fix MSYS2/Git Bash path auto-conversion on Windows:
// "/v1/owner/repo" is rewritten to "C:/Program Files/Git/v1/owner/repo".
// Detect the drive-letter prefix and restore the original API path.
if msysPathRe.MatchString(path) {
for _, prefix := range []string{"/v1/", "/v2/", "/api/", "/users/", "/projects/"} {
if idx := strings.Index(path, prefix); idx >= 0 {
path = path[idx:]
break
}
}
}
path = restoreAPIPath(path)
if !strings.HasPrefix(path, "/") {
path = "/" + path

View File

@ -402,3 +402,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)
}
})
}
}

View File

@ -0,0 +1,35 @@
# Fix: Windows Git Bash 下 `gitlink-cli api` 路径被 MSYS2 污染导致 404
## 问题
在 Windows Git BashMSYS2环境下执行
```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
```