From 789a3d3a4db7a33464aeb0f91b28c9e2dfe52443 Mon Sep 17 00:00:00 2001 From: Giteabot Date: Tue, 14 Apr 2026 02:01:44 +0800 Subject: [PATCH] fix(api): handle fork-only commits in compare API (#37185) (#37199) Backport #37185 by @Mohit25022005 Fix 500 error when comparing branches across fork repositories ## Problem The compare API returns a 500 Internal Server Error when comparing branches where the head commit exists only in the fork repository. ## Cause The API was using the base repository's GitRepo and repository context when converting commits. This fails when the commit does not exist in the base repository, resulting in a "fatal: bad object" error. ## Solution Use the head repository and HeadGitRepo when available to ensure commits are resolved in the correct repository context. ## Result * Fixes "fatal: bad object" error * Enables proper comparison between base and fork repositories * Prevents 500 Internal Server Error Fixes #37168 Co-authored-by: Mohit Swarnkar Co-authored-by: wxiaoguang --- routers/api/v1/repo/compare.go | 11 +++- tests/integration/api_repo_compare_test.go | 58 ++++++++++++++-------- 2 files changed, 45 insertions(+), 24 deletions(-) diff --git a/routers/api/v1/repo/compare.go b/routers/api/v1/repo/compare.go index 6285138c27..69dceb6db9 100644 --- a/routers/api/v1/repo/compare.go +++ b/routers/api/v1/repo/compare.go @@ -62,13 +62,20 @@ func CompareDiff(ctx *context.APIContext) { apiCommits := make([]*api.Commit, 0, len(compareInfo.Commits)) userCache := make(map[string]*user_model.User) + for i := 0; i < len(compareInfo.Commits); i++ { - apiCommit, err := convert.ToCommit(ctx, ctx.Repo.Repository, ctx.Repo.GitRepo, compareInfo.Commits[i], userCache, + apiCommit, err := convert.ToCommit( + ctx, + compareInfo.HeadRepo, + compareInfo.HeadGitRepo, + compareInfo.Commits[i], + userCache, convert.ToCommitOptions{ Stat: true, Verification: verification, Files: files, - }) + }, + ) if err != nil { ctx.APIErrorInternal(err) return diff --git a/tests/integration/api_repo_compare_test.go b/tests/integration/api_repo_compare_test.go index 9565e4d209..8aa0035b0a 100644 --- a/tests/integration/api_repo_compare_test.go +++ b/tests/integration/api_repo_compare_test.go @@ -5,46 +5,60 @@ package integration import ( "net/http" + "net/url" "testing" auth_model "code.gitea.io/gitea/models/auth" + repo_model "code.gitea.io/gitea/models/repo" "code.gitea.io/gitea/models/unittest" user_model "code.gitea.io/gitea/models/user" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/tests" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestAPICompareBranches(t *testing.T) { - defer tests.PrepareTestEnv(t)() + onGiteaRun(t, func(t *testing.T, _ *url.URL) { + session2 := loginUser(t, "user2") + token2 := getTokenForLoggedInUser(t, session2, auth_model.AccessTokenScopeWriteRepository) - user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) - // Login as User2. - session := loginUser(t, user.Name) - token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + t.Run("CompareBranches", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - t.Run("CompareBranches", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/add-csv...remove-files-b").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + 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, 2, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 2) + }) - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + t.Run("CompareCommits", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - assert.Equal(t, 2, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 2) - }) + req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").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) + }) - t.Run("CompareCommits", func(t *testing.T) { - defer tests.PrintCurrentTest(t)() - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo20/compare/808038d2f71b0ab02099...c8e31bc7688741a5287f").AddTokenAuth(token) - resp := MakeRequest(t, req, http.StatusOK) + t.Run("CompareForkOnlyCommit", func(t *testing.T) { + defer tests.PrintCurrentTest(t)() - var apiResp *api.Compare - DecodeJSON(t, resp, &apiResp) + user13 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 13}) + repo11 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 11}) + user13Sess := loginUser(t, "user13") + user13Token := getTokenForLoggedInUser(t, user13Sess, auth_model.AccessTokenScopeWriteRepository) - assert.Equal(t, 1, apiResp.TotalCommits) - assert.Len(t, apiResp.Commits, 1) + _, err := createFileInBranch(user13, repo11, createFileInBranchOptions{OldBranch: "master", NewBranch: "new-branch"}, map[string]string{"file.txt": "content"}) + require.NoError(t, err) + req := NewRequestf(t, "GET", "/api/v1/repos/user12/repo10/compare/master...user13:new-branch").AddTokenAuth(user13Token) + resp := MakeRequest(t, req, http.StatusOK) + apiResp := DecodeJSON(t, resp, &api.Compare{}) + assert.Equal(t, 1, apiResp.TotalCommits) + assert.Len(t, apiResp.Commits, 1) + }) }) }