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 <mohitswarnkar13@gmail.com>
Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
Giteabot
2026-04-14 02:01:44 +08:00
committed by GitHub
parent b37e098ff0
commit 789a3d3a4d
2 changed files with 45 additions and 24 deletions

View File

@@ -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

View File

@@ -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)
})
})
}