0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-05-10 20:31:25 +02:00

Fix compare dropdown for branches without common history (#37470)

This commit is contained in:
Nicolas 2026-04-28 23:03:50 +02:00 committed by GitHub
parent fedc9dc993
commit deec2b0929
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 160 additions and 154 deletions

View File

@ -4,9 +4,15 @@
package gitrepo package gitrepo
import ( import (
"path/filepath"
"strings"
"testing" "testing"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/util"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
type mockRepository struct { type mockRepository struct {
@ -17,6 +23,32 @@ func (r *mockRepository) RelativePath() string {
return r.path return r.path
} }
func TestMergeBaseNoCommonHistory(t *testing.T) {
repoDir := filepath.Join(t.TempDir(), "repo.git")
require.NoError(t, gitcmd.NewCommand("init").AddDynamicArguments(repoDir).Run(t.Context()))
_, _, runErr := gitcmd.NewCommand("fast-import").WithDir(repoDir).WithStdinBytes([]byte(strings.TrimSpace(`
commit refs/heads/branch1
committer User <user@example.com> 1714310400 +0000
data 12
First commit
M 100644 inline file1.txt
data 12
Hello from 1
commit refs/heads/branch2
committer User <user@example.com> 1714310400 +0000
data 13
Second commit
M 100644 inline file2.txt
data 12
Hello from 2
`))).RunStdString(t.Context())
require.NoError(t, runErr)
mergeBase, err := MergeBase(t.Context(), &mockRepository{path: repoDir}, "branch1", "branch2")
assert.Empty(t, mergeBase)
assert.ErrorIs(t, err, util.ErrNotExist)
}
func TestRepoGetDivergingCommits(t *testing.T) { func TestRepoGetDivergingCommits(t *testing.T) {
repo := &mockRepository{path: "repo1_bare"} repo := &mockRepository{path: "repo1_bare"}
do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2") do, err := GetDivergingCommits(t.Context(), repo, "master", "branch2")

View File

@ -9,13 +9,17 @@ import (
"strings" "strings"
"code.gitea.io/gitea/modules/git/gitcmd" "code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/util"
) )
// MergeBase checks and returns merge base of two commits. // MergeBase checks and returns merge base of two commits.
func MergeBase(ctx context.Context, repo Repository, baseCommitID, headCommitID string) (string, error) { func MergeBase(ctx context.Context, repo Repository, baseCommitID, headCommitID string) (string, error) {
mergeBase, _, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base"). mergeBase, stderr, err := RunCmdString(ctx, repo, gitcmd.NewCommand("merge-base").
AddDashesAndList(baseCommitID, headCommitID)) AddDashesAndList(baseCommitID, headCommitID))
if err != nil { if err != nil {
if gitcmd.IsErrorExitCode(err, 1) && strings.TrimSpace(stderr) == "" {
return "", util.NewNotExistErrorf("merge-base for %s and %s doesn't exist", baseCommitID, headCommitID)
}
return "", fmt.Errorf("get merge-base of %s and %s failed: %w", baseCommitID, headCommitID, err) return "", fmt.Errorf("get merge-base of %s and %s failed: %w", baseCommitID, headCommitID, err)
} }
return strings.TrimSpace(mergeBase), nil return strings.TrimSpace(mergeBase), nil

View File

@ -1784,6 +1784,7 @@
"repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff", "repo.pulls.review_only_possible_for_full_diff": "Review is only possible when viewing the full diff",
"repo.pulls.filter_changes_by_commit": "Filter by commit", "repo.pulls.filter_changes_by_commit": "Filter by commit",
"repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.", "repo.pulls.nothing_to_compare": "These branches are equal. There is no need to create a pull request.",
"repo.pulls.no_common_history": "These branches do not share a common merge base. Select a different base or compare branch.",
"repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.", "repo.pulls.nothing_to_compare_have_tag": "The selected branches/tags are equal.",
"repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.", "repo.pulls.nothing_to_compare_and_allow_empty_pr": "These branches are equal. This PR will be empty.",
"repo.pulls.has_pull_request": "A pull request between these branches already exists: <a href=\"%[1]s\">%[2]s#%[3]d</a>", "repo.pulls.has_pull_request": "A pull request between these branches already exists: <a href=\"%[1]s\">%[2]s#%[3]d</a>",

View File

@ -189,8 +189,8 @@ func setCsvCompareContext(ctx *context.Context) {
} }
} }
// ParseCompareInfo parse compare info between two commit for preparing comparing references // parseCompareInfo parse compare info between two commit for preparing comparing references
func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo { func parseCompareInfo(ctx *context.Context) (*git_service.CompareInfo, error) {
baseRepo := ctx.Repo.Repository baseRepo := ctx.Repo.Repository
fileOnly := ctx.FormBool("file-only") fileOnly := ctx.FormBool("file-only")
@ -199,47 +199,29 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
// remove the check when we support compare with carets // remove the check when we support compare with carets
if compareReq.BaseOriRefSuffix != "" { if compareReq.BaseOriRefSuffix != "" {
ctx.HTTPError(http.StatusBadRequest, "Unsupported comparison syntax: ref with suffix") return nil, util.NewInvalidArgumentErrorf("unsupported comparison syntax: ref with suffix")
return nil
} }
// 2 get repository and owner for head // 2 get repository and owner for head
headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq) headOwner, headRepo, err := common.GetHeadOwnerAndRepo(ctx, baseRepo, compareReq)
switch { if err != nil {
case errors.Is(err, util.ErrInvalidArgument): return nil, err
ctx.HTTPError(http.StatusBadRequest, err.Error())
return nil
case errors.Is(err, util.ErrNotExist):
ctx.NotFound(nil)
return nil
case err != nil:
ctx.ServerError("GetHeadOwnerAndRepo", err)
return nil
} }
isSameRepo := baseRepo.ID == headRepo.ID
// 3 permission check // 3 permission check
// base repository's code unit read permission check has been done on web.go // base repository's code unit read permission check has been done on web.go
permBase := ctx.Repo.Permission permBase := ctx.Repo.Permission
// If we're not merging from the same repo: // If we're not merging from the same repo:
isSameRepo := baseRepo.ID == headRepo.ID
if !isSameRepo { if !isSameRepo {
// Assert ctx.Doer has permission to read headRepo's codes // Assert ctx.Doer has permission to read headRepo's codes
permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer) permHead, err := access_model.GetDoerRepoPermission(ctx, headRepo, ctx.Doer)
if err != nil { if err != nil {
ctx.ServerError("GetDoerRepoPermission", err) return nil, err
return nil
} }
if !permHead.CanRead(unit.TypeCode) { if !permHead.CanRead(unit.TypeCode) {
if log.IsTrace() { return nil, util.NewNotExistErrorf("") // permission: no error message for end users
log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
ctx.Doer,
headRepo,
permHead)
}
ctx.NotFound(nil)
return nil
} }
ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode) ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
} }
@ -250,24 +232,17 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName) baseRef := ctx.Repo.GitRepo.UnstableGuessRefByShortName(baseRefName)
if baseRef == "" { if baseRef == "" {
ctx.NotFound(nil) return nil, util.NewNotExistErrorf("no base ref: %s", baseRefName)
return nil
} }
var headGitRepo *git.Repository headGitRepo, err := gitrepo.RepositoryFromRequestContextOrOpen(ctx, headRepo)
if isSameRepo { if err != nil {
headGitRepo = ctx.Repo.GitRepo ctx.ServerError("OpenRepository", err)
} else { return nil, err
headGitRepo, err = gitrepo.OpenRepository(ctx, headRepo)
if err != nil {
ctx.ServerError("OpenRepository", err)
return nil
}
defer headGitRepo.Close()
} }
headRef := headGitRepo.UnstableGuessRefByShortName(headRefName) headRef := headGitRepo.UnstableGuessRefByShortName(headRefName)
if headRef == "" { if headRef == "" {
ctx.NotFound(nil) return nil, util.NewNotExistErrorf("no head ref: %s", headRefName)
return nil
} }
ctx.Data["BaseName"] = baseRepo.OwnerName ctx.Data["BaseName"] = baseRepo.OwnerName
@ -291,12 +266,9 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
var rootRepo *repo_model.Repository var rootRepo *repo_model.Repository
if baseRepo.IsFork { if baseRepo.IsFork {
err = baseRepo.GetBaseRepo(ctx) err = baseRepo.GetBaseRepo(ctx)
if err != nil { if err != nil && !repo_model.IsErrRepoNotExist(err) {
if !repo_model.IsErrRepoNotExist(err) { return nil, err
ctx.ServerError("Unable to find root repo", err) } else if err == nil {
return nil
}
} else {
rootRepo = baseRepo.BaseRepo rootRepo = baseRepo.BaseRepo
} }
} }
@ -313,42 +285,10 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
} }
} }
has := headRepo != nil
// 3. If the base is a forked from "RootRepo" and the owner of
// the "RootRepo" is the :headUser - set headRepo to that
if !has && rootRepo != nil && rootRepo.OwnerID == headOwner.ID {
headRepo = rootRepo
has = true
}
// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
// set the headRepo to the ownFork
if !has && ownForkRepo != nil && ownForkRepo.OwnerID == headOwner.ID {
headRepo = ownForkRepo
has = true
}
// 5. If the headOwner has a fork of the baseRepo - use that
if !has {
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ID)
has = headRepo != nil
}
// 6. If the baseRepo is a fork and the headUser has a fork of that use that
if !has && baseRepo.IsFork {
headRepo = repo_model.GetForkedRepo(ctx, headOwner.ID, baseRepo.ForkID)
has = headRepo != nil
}
// 7. Otherwise if we're not the same repo and haven't found a repo give up
if !isSameRepo && !has {
ctx.Data["PageIsComparePull"] = false
}
ctx.Data["HeadRepo"] = headRepo ctx.Data["HeadRepo"] = headRepo
ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
// If we have a rootRepo and it's different from: // If we have a rootRepo, and it's different from:
// 1. the computed base // 1. the computed base
// 2. the computed head // 2. the computed head
// then get the branches of it // then get the branches of it
@ -361,17 +301,15 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
if !fileOnly { if !fileOnly {
branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo) branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
if err != nil { if err != nil {
ctx.ServerError("GetBranchesForRepo", err) return nil, err
return nil
} }
ctx.Data["RootRepoBranches"] = branches ctx.Data["RootRepoBranches"] = branches
ctx.Data["RootRepoTags"] = tags ctx.Data["RootRepoTags"] = tags
} }
} }
} }
// If we have a ownForkRepo and it's different from: // If we have a ownForkRepo, and it's different from:
// 1. The computed base // 1. The computed base
// 2. The computed head // 2. The computed head
// 3. The rootRepo (if we have one) // 3. The rootRepo (if we have one)
@ -386,8 +324,7 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
if !fileOnly { if !fileOnly {
branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo) branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
if err != nil { if err != nil {
ctx.ServerError("GetBranchesForRepo", err) return nil, err
return nil
} }
ctx.Data["OwnForkRepoBranches"] = branches ctx.Data["OwnForkRepoBranches"] = branches
ctx.Data["OwnForkRepoTags"] = tags ctx.Data["OwnForkRepoTags"] = tags
@ -395,33 +332,21 @@ func ParseCompareInfo(ctx *context.Context) *git_service.CompareInfo {
} }
} }
// Treat as pull request if both references are branches
if ctx.Data["PageIsComparePull"] == nil {
ctx.Data["PageIsComparePull"] = baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
}
if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
if log.IsTrace() {
log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
ctx.Doer,
baseRepo,
permBase)
}
ctx.NotFound(nil)
return nil
}
compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly) compareInfo, err := git_service.GetCompareInfo(ctx, baseRepo, headRepo, headGitRepo, baseRef, headRef, compareReq.DirectComparison(), fileOnly)
if err != nil { if err != nil {
ctx.ServerError("GetCompareInfo", err) return nil, err
return nil
} }
// Treat as pull request if both references are branches
allowCreatePullRequest := baseRef.IsBranch() && headRef.IsBranch() && permBase.CanReadIssuesOrPulls(true)
allowCreatePullRequest = allowCreatePullRequest && compareInfo.MergeBase != ""
ctx.Data["PageIsComparePull"] = allowCreatePullRequest
if compareReq.DirectComparison() { if compareReq.DirectComparison() {
ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID ctx.Data["BeforeCommitID"] = compareInfo.BaseCommitID
} else { } else {
ctx.Data["BeforeCommitID"] = compareInfo.MergeBase ctx.Data["BeforeCommitID"] = compareInfo.MergeBase
} }
return &compareInfo return &compareInfo, nil
} }
func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) { func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*git_model.SignCommitWithStatuses) (title, content string) {
@ -454,12 +379,11 @@ func prepareNewPullRequestTitleContent(ci *git_service.CompareInfo, commits []*g
return title, content return title, content
} }
// PrepareCompareDiff renders compare diff page // prepareCompareDiff renders compare diff page. TODO: need to refactor it and other "compare diff" related functions together
func PrepareCompareDiff( func prepareCompareDiff(ctx *context.Context, ci *git_service.CompareInfo, whitespaceBehavior gitcmd.TrustedCmdArgs) (nothingToCompare bool) {
ctx *context.Context, if ci.MergeBase == "" {
ci *git_service.CompareInfo, return true
whitespaceBehavior gitcmd.TrustedCmdArgs, }
) (nothingToCompare bool) {
repo := ctx.Repo.Repository repo := ctx.Repo.Repository
headCommitID := ci.HeadCommitID headCommitID := ci.HeadCommitID
@ -568,9 +492,6 @@ func PrepareCompareDiff(
ctx.Data["CommitCount"] = len(commits) ctx.Data["CommitCount"] = len(commits)
ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits) ctx.Data["title"], ctx.Data["content"] = prepareNewPullRequestTitleContent(ci, commits)
ctx.Data["Username"] = ci.HeadRepo.OwnerName
ctx.Data["Reponame"] = ci.HeadRepo.Name
setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name) setCompareContext(ctx, beforeCommit, headCommit, ci.HeadRepo.OwnerName, repo.Name)
return false return false
@ -594,16 +515,24 @@ func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repositor
// CompareDiff show different from one commit to another commit // CompareDiff show different from one commit to another commit
func CompareDiff(ctx *context.Context) { func CompareDiff(ctx *context.Context) {
ci := ParseCompareInfo(ctx) ci, err := parseCompareInfo(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if errors.Is(err, util.ErrNotExist) || errors.Is(err, util.ErrInvalidArgument) {
ctx.NotFound(nil)
return
} else if err != nil {
ctx.ServerError("ParseCompareInfo", err)
return
}
ctx.Data["PageIsViewCode"] = true ctx.Data["PageIsViewCode"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["CompareInfo"] = ci ctx.Data["CompareInfo"] = ci
nothingToCompare := PrepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string))) // TODO: need to refactor "prepare compare" related functions together
nothingToCompare := prepareCompareDiff(ctx, ci, gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
if ctx.Written() { if ctx.Written() {
return return
} }
@ -621,16 +550,13 @@ func CompareDiff(ctx *context.Context) {
return return
} }
headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{ headBranches, headTags, err := getBranchesAndTagsForRepo(ctx, ci.HeadRepo)
RepoID: ci.HeadRepo.ID,
ListOptions: db.ListOptionsAll,
IsDeletedBranch: optional.Some(false),
})
if err != nil { if err != nil {
ctx.ServerError("GetBranches", err) ctx.ServerError("GetBranchesAndTagsForRepo", err)
return return
} }
ctx.Data["HeadBranches"] = headBranches ctx.Data["HeadBranches"] = headBranches
ctx.Data["HeadTags"] = headTags
// For compare repo branches // For compare repo branches
PrepareBranchList(ctx) PrepareBranchList(ctx)
@ -638,13 +564,20 @@ func CompareDiff(ctx *context.Context) {
return return
} }
headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID) if ci.MergeBase != "" {
if err != nil { prepareCreatePullRequestPage(ctx, ci, nothingToCompare)
ctx.ServerError("GetTagNamesByRepoID", err) if ctx.Written() {
return return
}
} else {
ctx.Flash.Error(ctx.Tr("repo.pulls.no_common_history"), true)
ctx.Data["PageIsComparePull"] = false
ctx.Data["CommitCount"] = 0
} }
ctx.Data["HeadTags"] = headTags ctx.HTML(http.StatusOK, tplCompare)
}
func prepareCreatePullRequestPage(ctx *context.Context, ci *git_service.CompareInfo, nothingToCompare bool) {
if ctx.Data["PageIsComparePull"] == true { if ctx.Data["PageIsComparePull"] == true {
pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub) pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadRef.ShortName(), ci.BaseRef.ShortName(), issues_model.PullRequestFlowGithub)
if err != nil { if err != nil {
@ -685,7 +618,7 @@ func CompareDiff(ctx *context.Context) {
if content, ok := ctx.Data["content"].(string); ok && content != "" { if content, ok := ctx.Data["content"].(string); ok && content != "" {
// If a template content is set, prepend the "content". In this case that's only // If a template content is set, prepend the "content". In this case that's only
// applicable if you have one commit to compare and that commit has a message. // applicable if you have one commit to compare and that commit has a message.
// In that case the commit message will be prepend to the template body. // In that case the commit message will be prepended to the template body.
if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" { if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
// Re-use the same key as that's prioritized over the "content" key. // Re-use the same key as that's prioritized over the "content" key.
// Add two new lines between the content to ensure there's always at least // Add two new lines between the content to ensure there's always at least
@ -713,14 +646,8 @@ func CompareDiff(ctx *context.Context) {
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests) ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil { prConfig := ctx.Repo.Repository.MustGetUnit(ctx, unit.TypePullRequests).PullRequestsConfig()
config := unit.PullRequestsConfig() ctx.Data["AllowMaintainerEdit"] = prConfig.DefaultAllowMaintainerEdit
ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit
} else {
ctx.Data["AllowMaintainerEdit"] = false
}
ctx.HTML(http.StatusOK, tplCompare)
} }
// attachCommentsToLines attaches comments to their corresponding diff lines // attachCommentsToLines attaches comments to their corresponding diff lines

View File

@ -1281,24 +1281,26 @@ func PullsNewRedirect(ctx *context.Context) {
// CompareAndPullRequestPost response for creating pull request // CompareAndPullRequestPost response for creating pull request
func CompareAndPullRequestPost(ctx *context.Context) { func CompareAndPullRequestPost(ctx *context.Context) {
form := web.GetForm(ctx).(*forms.CreateIssueForm) form := web.GetForm(ctx).(*forms.CreateIssueForm)
ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes") repo := ctx.Repo.Repository
ctx.Data["PageIsComparePull"] = true
ctx.Data["IsDiffCompare"] = true
ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
upload.AddUploadContext(ctx, "comment")
ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.Permission.CanWrite(unit.TypePullRequests)
var ( ci, err := parseCompareInfo(ctx)
repo = ctx.Repo.Repository
attachments []string
)
ci := ParseCompareInfo(ctx)
if ctx.Written() { if ctx.Written() {
return return
} }
if errors.Is(err, util.ErrNotExist) {
ctx.JSONErrorNotFound()
return
} else if errors.Is(err, util.ErrInvalidArgument) {
ctx.JSONError(err.Error())
return
} else if err != nil {
ctx.ServerError("ParseCompareInfo", err)
return
}
if ci.MergeBase == "" {
ctx.JSONError(ctx.Tr("repo.pulls.no_common_history"))
return
}
validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true) validateRet := ValidateRepoMetasForNewIssue(ctx, *form, true)
if ctx.Written() { if ctx.Written() {
return return
@ -1306,6 +1308,7 @@ func CompareAndPullRequestPost(ctx *context.Context) {
labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID labelIDs, assigneeIDs, milestoneID, projectID := validateRet.LabelIDs, validateRet.AssigneeIDs, validateRet.MilestoneID, validateRet.ProjectID
var attachments []string
if setting.Attachment.Enabled { if setting.Attachment.Enabled {
attachments = form.Files attachments = form.Files
} }

View File

@ -45,6 +45,7 @@ func (ci *CompareInfo) DirectComparison() bool {
// GetCompareInfo generates and returns compare information between base and head branches of repositories. // GetCompareInfo generates and returns compare information between base and head branches of repositories.
// It does its best to fill the fields as many as it can. // It does its best to fill the fields as many as it can.
// MergeBase can be empty if the base and head are unrelated.
func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (compareInfo CompareInfo, err error) { func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Repository, headGitRepo *git.Repository, baseRef, headRef git.RefName, directComparison, fileOnly bool) (compareInfo CompareInfo, err error) {
baseCommitID, err1 := gitrepo.GetFullCommitID(ctx, baseRepo, baseRef.String()) baseCommitID, err1 := gitrepo.GetFullCommitID(ctx, baseRepo, baseRef.String())
headCommitID, err2 := gitrepo.GetFullCommitID(ctx, headRepo, headRef.String()) headCommitID, err2 := gitrepo.GetFullCommitID(ctx, headRepo, headRef.String())
@ -75,13 +76,17 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
if !directComparison { if !directComparison {
compareInfo.MergeBase, err = gitrepo.MergeBase(ctx, headRepo, compareInfo.BaseCommitID, compareInfo.HeadCommitID) compareInfo.MergeBase, err = gitrepo.MergeBase(ctx, headRepo, compareInfo.BaseCommitID, compareInfo.HeadCommitID)
if err != nil { if err != nil && !errors.Is(err, util.ErrNotExist) {
return compareInfo, fmt.Errorf("MergeBase: %w", err) return compareInfo, fmt.Errorf("MergeBase: %w", err)
} }
} else { } else {
compareInfo.MergeBase = compareInfo.BaseCommitID compareInfo.MergeBase = compareInfo.BaseCommitID
} }
if compareInfo.MergeBase == "" {
return compareInfo, nil
}
// We have a common base - therefore we know that ... should work // We have a common base - therefore we know that ... should work
if !fileOnly { if !fileOnly {
// In git log/rev-list, the "..." syntax represents the symmetric difference between two references, // In git log/rev-list, the "..." syntax represents the symmetric difference between two references,
@ -92,12 +97,10 @@ func GetCompareInfo(ctx context.Context, baseRepo, headRepo *repo_model.Reposito
if err != nil { if err != nil {
return compareInfo, fmt.Errorf("ShowPrettyFormatLogToList: %w", err) return compareInfo, fmt.Errorf("ShowPrettyFormatLogToList: %w", err)
} }
} else {
compareInfo.Commits = []*git.Commit{}
} }
// Count number of changed files. // Count number of changed files.
// This probably should be removed as we need to use shortstat elsewhere // TODO: This probably should be removed as we need to use shortstat elsewhere
// Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly // Now there is git diff --shortstat but this appears to be slower than simply iterating with --nameonly
compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison) compareInfo.NumFiles, err = headGitRepo.GetDiffNumChangedFiles(compareInfo.BaseCommitID, compareInfo.HeadCommitID, directComparison)
return compareInfo, err return compareInfo, err

View File

@ -13,6 +13,7 @@
{{ctx.Locale.Tr "action.compare_commits_general"}} {{ctx.Locale.Tr "action.compare_commits_general"}}
{{end}} {{end}}
</h2> </h2>
{{template "base/alert" .}}
{{$BaseCompareName := $.Repository.FullName -}} {{$BaseCompareName := $.Repository.FullName -}}
{{$HeadCompareName := $.HeadRepo.FullName -}} {{$HeadCompareName := $.HeadRepo.FullName -}}
{{$OwnForkCompareName := "" -}} {{$OwnForkCompareName := "" -}}

View File

@ -10,13 +10,16 @@ import (
"strings" "strings"
"testing" "testing"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unittest" "code.gitea.io/gitea/models/unittest"
user_model "code.gitea.io/gitea/models/user" user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git/gitcmd"
"code.gitea.io/gitea/modules/test" "code.gitea.io/gitea/modules/test"
repo_service "code.gitea.io/gitea/services/repository" repo_service "code.gitea.io/gitea/services/repository"
"code.gitea.io/gitea/tests" "code.gitea.io/gitea/tests"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestCompareTag(t *testing.T) { func TestCompareTag(t *testing.T) {
@ -124,6 +127,38 @@ func TestCompareBranches(t *testing.T) {
inspectCompare(t, htmlDoc, diffCount, diffChanges) inspectCompare(t, htmlDoc, diffCount, diffChanges)
} }
func TestCompareBranchesNoCommonMergeBase(t *testing.T) {
defer tests.PrepareTestEnv(t)()
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "user2"})
repo1 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{OwnerID: user2.ID, Name: "repo1"})
repoPath := repo_model.RepoPath(user2.Name, repo1.Name)
_, _, runErr := gitcmd.NewCommand("fast-import").WithDir(repoPath).WithStdinBytes([]byte(strings.TrimSpace(`
commit refs/heads/unrelated-history
committer User <user@example.com> 1714310400 +0000
data 13
Second commit
M 100644 inline file2.txt
data 12
Hello from 2
`))).RunStdString(t.Context())
require.NoError(t, runErr)
session := loginUser(t, "user2")
req := NewRequest(t, "GET", "/user2/repo1/compare/master...unrelated-history")
resp := session.MakeRequest(t, req, http.StatusOK)
body := resp.Body.String()
htmlDoc := NewHTMLParser(t, resp.Body)
selection := htmlDoc.doc.Find(".ui.dropdown.select-branch")
assert.Lenf(t, selection.Nodes, 2, "The template has changed")
assert.Contains(t, body, "These branches do not share a common merge base")
assert.Equal(t, 1, htmlDoc.doc.Find(`a.item[href="/user2/repo1/compare/master...unrelated-history"]`).Length())
assert.Equal(t, 1, htmlDoc.doc.Find(`a.item[href="/user2/repo1/compare/master...master"]`).Length())
assert.Equal(t, 0, htmlDoc.doc.Find(".pullrequest-form").Length())
}
func TestCompareCodeExpand(t *testing.T) { func TestCompareCodeExpand(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) { onGiteaRun(t, func(t *testing.T, u *url.URL) {
user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1}) user1 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})