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 08:31:58 +03:00
committed by GitHub
parent 4da9b59414
commit cebdc90ed9
3 changed files with 140 additions and 96 deletions
+40 -12
View File
@@ -5,6 +5,7 @@ package utils
import (
"errors"
"strings"
git_model "gitea.dev/models/git"
repo_model "gitea.dev/models/repo"
@@ -20,23 +21,50 @@ type RefCommit struct {
CommitID string
}
// ResolveRefCommit resolve ref to a commit if exist
// ResolveRefCommit resolve ref to a commit if it exists.
// inputRef may be a short branch/tag name, a commit ID, or a fully-qualified
// git ref (e.g. refs/heads/main, refs/tags/v1.0) for GitHub client compatibility.
func ResolveRefCommit(ctx reqctx.RequestContext, repo *repo_model.Repository, inputRef string, minCommitIDLen ...int) (_ *RefCommit, err error) {
refCommit := RefCommit{InputRef: inputRef}
var testRefBranch, testRefTag git.RefName
if strings.HasPrefix(inputRef, "refs/") {
// Fully-qualified refs first so clients can pass unambiguous names.
testRef := git.RefName(inputRef)
if testRef.IsBranch() {
testRefBranch = testRef
} else if testRef.IsTag() {
testRefTag = testRef
}
} else {
testRefBranch, testRefTag = git.RefNameFromBranch(inputRef), git.RefNameFromTag(inputRef)
}
if testRefBranch != "" {
if exist, _ := git_model.IsBranchExist(ctx, repo.ID, testRefBranch.ShortName()); exist {
refCommit.RefName = testRefBranch
}
}
if testRefTag != "" {
// TODO: use git model instead of git command in the future? Model seems to be faster.
if git.IsTagExist(ctx, repo, testRefTag.ShortName()) {
refCommit.RefName = testRefTag
}
}
if refCommit.RefName == "" {
if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.ObjectFormatName), inputRef, minCommitIDLen...) {
refCommit.RefName = git.RefNameFromCommit(inputRef)
}
}
if refCommit.RefName == "" {
return nil, git.ErrNotExist{ID: inputRef}
}
gitRepo, err := git.RepositoryFromRequestContextOrOpen(ctx, repo)
if err != nil {
return nil, err
}
refCommit := RefCommit{InputRef: inputRef}
if exist, _ := git_model.IsBranchExist(ctx, repo.ID, inputRef); exist {
refCommit.RefName = git.RefNameFromBranch(inputRef)
} else if git.IsTagExist(ctx, repo, inputRef) {
refCommit.RefName = git.RefNameFromTag(inputRef)
} else if git.IsStringLikelyCommitID(git.ObjectFormatFromName(repo.ObjectFormatName), inputRef, minCommitIDLen...) {
refCommit.RefName = git.RefNameFromCommit(inputRef)
}
if refCommit.RefName == "" {
return nil, git.ErrNotExist{ID: inputRef}
}
if refCommit.Commit, err = gitRepo.GetCommit(ctx, refCommit.RefName.String()); err != nil {
return nil, err
}