mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-25 21:09:44 +09:00
feat(api): support ref suffixes in compare (#38148)
Compare API requests with a `^` or `~N` revision suffix (for example `compare/main...feature^`) were rejected with `400 Unsupported comparison syntax: ref with suffix`. The fix resolves the suffix to a commit before comparing, so `base...head^` and `~N` work on either side, the same as git. Only `^`/`~N` navigation is resolved. Pull request creation still requires plain branch refs, and the web compare page keeps rejecting suffixes since its branch selectors need separate UI work. Closes #33943
This commit is contained in:
@@ -45,6 +45,40 @@ func TestAPICompareBranches(t *testing.T) {
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
})
|
||||
|
||||
t.Run("CompareWithRefSuffix", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
// remove-files-b^ is the parent of the tip, so the range drops the tip and ends at that parent
|
||||
const parentSHA = "b67e43a07d48243a5f670ace063acd5e13f719df"
|
||||
req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b^").AddTokenAuth(token2)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
apiResp := DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
assert.Equal(t, parentSHA, apiResp.Commits[0].SHA)
|
||||
|
||||
// the same suffix on the direct ".." comparison resolves to the same commit
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv..remove-files-b^").AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiResp = DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Equal(t, parentSHA, apiResp.Commits[0].SHA)
|
||||
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv~1...add-csv").AddTokenAuth(token2)
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
apiResp = DecodeJSON(t, resp, &api.Compare{})
|
||||
assert.Equal(t, 1, apiResp.TotalCommits)
|
||||
assert.Len(t, apiResp.Commits, 1)
|
||||
|
||||
// a valid but unresolvable suffix is not found, while an unsupported suffix (^{...}) is a bad request
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b~50").AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b^{/Add}").AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
req = NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv^{/Add}...remove-files-b").AddTokenAuth(token2)
|
||||
MakeRequest(t, req, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("CompareForkOnlyCommit", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
|
||||
@@ -14,7 +14,10 @@ import (
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
"gitea.dev/modules/git/gitcmd"
|
||||
"gitea.dev/modules/gitrepo"
|
||||
"gitea.dev/modules/test"
|
||||
"gitea.dev/modules/util"
|
||||
"gitea.dev/routers/common"
|
||||
repo_service "gitea.dev/services/repository"
|
||||
"gitea.dev/tests"
|
||||
|
||||
@@ -135,6 +138,73 @@ func TestCompareBranches(t *testing.T) {
|
||||
inspectCompare(t, htmlDoc, diffCount, diffChanges)
|
||||
}
|
||||
|
||||
func TestCompareWithRefSuffix(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user2")
|
||||
|
||||
// remove-files-b^ resolves to the tip's parent, so the test.txt added by the tip is excluded
|
||||
req := NewRequest(t, "GET", "/user2/repo20/compare/add-csv...remove-files-b^")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
inspectCompare(t, htmlDoc, 2, []string{"link_hi", "test.csv"})
|
||||
|
||||
// a suffix resolves to a commit rather than a branch, so the page offers no pull request to create
|
||||
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length())
|
||||
|
||||
// the same suffix on the direct ".." comparison resolves to the same commit
|
||||
req = NewRequest(t, "GET", "/user2/repo20/compare/add-csv..remove-files-b^")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
inspectCompare(t, htmlDoc, 2, []string{"link_hi", "test.csv"})
|
||||
|
||||
// a ~N suffix on the base side resolves and renders the compare page, but also
|
||||
// resolves to a commit rather than a branch, so no pull request form is offered
|
||||
req = NewRequest(t, "GET", "/user2/repo20/compare/add-csv~1...remove-files-b")
|
||||
resp = session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
|
||||
htmlDoc = NewHTMLParser(t, resp.Body)
|
||||
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length())
|
||||
|
||||
// the web handler folds an unsupported (^{...}) and an unresolvable (~50) suffix alike into 404
|
||||
for _, basehead := range []string{
|
||||
"add-csv...remove-files-b~50",
|
||||
"add-csv...remove-files-b^{/Add}",
|
||||
"add-csv^{/Add}...remove-files-b",
|
||||
} {
|
||||
req = NewRequest(t, "GET", "/user2/repo20/compare/"+basehead).SetHeader("Accept", "text/html")
|
||||
resp = session.MakeRequest(t, req, http.StatusNotFound)
|
||||
assert.True(t, test.IsNormalPageCompleted(resp.Body.String()))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveRefWithSuffixContract(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 31})
|
||||
gitRepo, err := gitrepo.OpenRepository(t.Context(), repo)
|
||||
require.NoError(t, err)
|
||||
defer gitRepo.Close()
|
||||
|
||||
// a nil error guarantees a usable RefName
|
||||
ref, err := common.ResolveRefWithSuffix(gitRepo, "add-csv", "^")
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, ref)
|
||||
// a ref resolved with a suffix must be a commit SHA, not a branch ref
|
||||
// (branch refs would break "New Pull Request" logic)
|
||||
assert.False(t, ref.IsBranch(), "ref with suffix must not resolve to a branch")
|
||||
|
||||
// a missing ref and an unresolvable suffix both report not-found instead of an empty RefName
|
||||
for _, tc := range []struct{ oriRef, suffix string }{
|
||||
{"does-not-exist", ""},
|
||||
{"add-csv", "~50"},
|
||||
} {
|
||||
ref, err := common.ResolveRefWithSuffix(gitRepo, tc.oriRef, tc.suffix)
|
||||
assert.ErrorIs(t, err, util.ErrNotExist, "ref %q suffix %q", tc.oriRef, tc.suffix)
|
||||
assert.Empty(t, ref, "ref %q suffix %q", tc.oriRef, tc.suffix)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareBranchesNoCommonMergeBase(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user