fix(api): accept fully-qualified refs in contents API (#38650)

`GET/POST` repository contents endpoints resolve the `ref` query
parameter through `ResolveRefCommit`, which previously only accepted
short branch/tag names or commit IDs. Clients that pass fully-qualified
Git refs (GitHub-compatible), e.g. `ref=refs%2Fheads%2Fmain` or
`ref=refs%2Ftags%2Fv1.0`, received 404 "object does not exist".

This change accepts fully-qualified **`refs/heads/*`** and
**`refs/tags/*`** only, then falls back to the existing short-name and
SHA logic. Other `refs/*` prefixes (e.g. `refs/pull/`, `refs/for/`) are
rejected.

Fixes #38197

---------

Co-authored-by: roman s <roman.sukach@dust-labs.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
roman.s
2026-07-27 05:31:58 +00:00
committed by GitHub
co-authored by roman s wxiaoguang
parent 4da9b59414
commit cebdc90ed9
3 changed files with 140 additions and 96 deletions
+23 -6
View File
@@ -63,33 +63,50 @@ func TestAPIGetRequestedFiles(t *testing.T) {
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/file-contents?body="+url.QueryEscape(string(reqBodyParam)))
resp := MakeRequest(t, req, http.StatusOK)
ret := DecodeJSON(t, resp, []*api.ContentsResponse{})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents(repo1.DefaultBranch, "branch", lastCommit.ID.String())}
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("master", "refs/heads/master", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2NoRef", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents(repo1.DefaultBranch, "branch", lastCommit.ID.String())}
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("master", "refs/heads/master", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefBranch", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=master", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents(repo1.DefaultBranch, "branch", lastCommit.ID.String())}
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("master", "refs/heads/master", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefTag", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=v1.1", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("v1.1", "tag", lastCommit.ID.String())}
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("v1.1", "refs/tags/v1.1", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefCommit", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=65f1bf27bc3bf70f64657658635e66094edbcb4d", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("65f1bf27bc3bf70f64657658635e66094edbcb4d", "commit", lastCommit.ID.String())}
commitID := "65f1bf27bc3bf70f64657658635e66094edbcb4d"
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref="+commitID, []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents(commitID, git.RefNameFromCommit(commitID), lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefNotExist", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=not-exist", []string{"README.md"}, http.StatusNotFound)
assert.Empty(t, ret)
})
t.Run("User2RefFullBranch", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=refs/heads/master", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("refs/heads/master", "refs/heads/master", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefFullTag", func(t *testing.T) {
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref=refs/tags/v1.1", []string{"README.md"})
expected := []*api.ContentsResponse{getExpectedContentsResponseForContents("refs/tags/v1.1", "refs/tags/v1.1", lastCommit.ID.String())}
assert.Equal(t, expected, ret)
})
t.Run("User2RefInternalPullRejected", func(t *testing.T) {
// repo1 has refs/pull/2/head in fixtures; contents API must not resolve it
assert.True(t, git.IsReferenceExist(t.Context(), gitRepo, "refs/pull/2/head"))
ret := requestFiles(t, "/api/v1/repos/user2/repo1/file-contents?ref="+url.QueryEscape("refs/pull/2/head"), []string{"README.md"}, http.StatusNotFound)
assert.Empty(t, ret)
})
t.Run("PermissionCheck", func(t *testing.T) {
filesOptions := &api.GetFilesOptions{Files: []string{"README.md"}}