Refactor calc mergebase

This commit is contained in:
Lunny Xiao 2025-07-14 12:07:12 -07:00
parent f8cea67265
commit eddd21bdea
10 changed files with 151 additions and 81 deletions

View File

@ -764,6 +764,10 @@ func (c *Comment) CodeCommentLink(ctx context.Context) string {
return fmt.Sprintf("%s/files#%s", c.Issue.Link(), c.HashTag())
}
func GetCodeCommentRef(prIndex, commentID int64) string {
return fmt.Sprintf("refs/pull/%d/code-comment-%d", prIndex, commentID)
}
// CreateComment creates comment with context
func CreateComment(ctx context.Context, opts *CreateCommentOptions) (_ *Comment, err error) {
ctx, committer, err := db.TxContext(ctx)
@ -1151,6 +1155,15 @@ func DeleteComment(ctx context.Context, comment *Comment) error {
return err
}
// delete review if the comment is the last comment of the review
if comment.ReviewID > 0 {
if _, err := db.GetEngine(ctx).ID(comment.ReviewID).
Where("(SELECT count(id) FROM comment WHERE review_id = ?) == 0", comment.ReviewID).
Delete(new(Review)); err != nil {
return err
}
}
if err := comment.neuterCrossReferences(ctx); err != nil {
return err
}

View File

@ -4,6 +4,7 @@
package git
import (
"context"
"regexp"
"strings"
@ -220,3 +221,14 @@ func (ref RefName) RefWebLinkPath() string {
}
return string(refType) + "/" + util.PathEscapeSegments(ref.ShortName())
}
func UpdateRef(ctx context.Context, repoPath, refName, newCommitID string) error {
_, _, err := NewCommand("update-ref").AddDynamicArguments(refName, newCommitID).RunStdString(ctx, &RunOpts{Dir: repoPath})
return err
}
func RemoveRef(ctx context.Context, repoPath, refName string) error {
_, _, err := NewCommand("update-ref", "--no-deref", "-d").
AddDynamicArguments(refName).RunStdString(ctx, &RunOpts{Dir: repoPath})
return err
}

View File

@ -50,18 +50,6 @@ func (repo *Repository) GetRefCommitID(name string) (string, error) {
return string(shaBs), nil
}
// SetReference sets the commit ID string of given reference (e.g. branch or tag).
func (repo *Repository) SetReference(name, commitID string) error {
_, _, err := NewCommand("update-ref").AddDynamicArguments(name, commitID).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
return err
}
// RemoveReference removes the given reference (e.g. branch or tag).
func (repo *Repository) RemoveReference(name string) error {
_, _, err := NewCommand("update-ref", "--no-deref", "-d").AddDynamicArguments(name).RunStdString(repo.Ctx, &RunOpts{Dir: repo.Path})
return err
}
// IsCommitExist returns true if given commit exists in current repository.
func (repo *Repository) IsCommitExist(name string) bool {
if err := ensureValidGitRepository(repo.Ctx, repo.Path); err != nil {

View File

@ -99,7 +99,7 @@ func TestReadWritePullHead(t *testing.T) {
// Write a fake sha1 with only 40 zeros
newCommit := "feaf4ba6bc635fec442f46ddd4512416ec43c2c2"
err = repo.SetReference(PullPrefix+"1/head", newCommit)
err = UpdateRef(t.Context(), repo.Path, PullPrefix+"1/head", newCommit)
if err != nil {
assert.NoError(t, err)
return
@ -116,7 +116,7 @@ func TestReadWritePullHead(t *testing.T) {
assert.Equal(t, headContents, newCommit)
// Remove file after the test
err = repo.RemoveReference(PullPrefix + "1/head")
err = RemoveRef(t.Context(), repo.Path, PullPrefix+"1/head")
assert.NoError(t, err)
}

View File

@ -190,7 +190,7 @@ func GetPullDiffStats(ctx *context.Context) {
}
pull := issue.PullRequest
mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
mergeBaseCommitID := GetMergedBaseCommitID(ctx, pull)
if mergeBaseCommitID == "" {
return // no merge base, do nothing, do not stop the route handler, see below
}
@ -210,48 +210,46 @@ func GetPullDiffStats(ctx *context.Context) {
ctx.Data["DiffShortStat"] = diffShortStat
}
func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) string {
pull := issue.PullRequest
func calcMergeBase(ctx *context.Context, pull *issues_model.PullRequest) string {
var commitSHA, parentCommit string
// If there is a head or a patch file, and it is readable, grab info
commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
// Head File does not exist, try the patch
// FIXME: it seems this patch file is not used in the code, but it is still read
commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index)
if err == nil {
// Recreate pull head in files for next time
if err := git.UpdateRef(ctx, ctx.Repo.GitRepo.Path, pull.GetGitHeadRefName(), commitSHA); err != nil {
log.Error("Could not write head file", err)
}
} else {
// There is no history available
log.Trace("No history file available for PR %d", pull.Index)
}
}
if commitSHA != "" {
// Get immediate parent of the first commit in the patch, grab history back
parentCommit, _, err = git.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(ctx, &git.RunOpts{Dir: ctx.Repo.GitRepo.Path})
if err == nil {
parentCommit = strings.TrimSpace(parentCommit)
}
// Special case on Git < 2.25 that doesn't fail on immediate empty history
if err != nil || parentCommit == "" {
log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
// bring at least partial history if it can work
parentCommit = commitSHA
}
}
return parentCommit
}
var baseCommit string
// Some migrated PR won't have any Base SHA and lose history, try to get one
if pull.MergeBase == "" {
var commitSHA, parentCommit string
// If there is a head or a patch file, and it is readable, grab info
commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitHeadRefName())
if err != nil {
// Head File does not exist, try the patch
commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index)
if err == nil {
// Recreate pull head in files for next time
if err := ctx.Repo.GitRepo.SetReference(pull.GetGitHeadRefName(), commitSHA); err != nil {
log.Error("Could not write head file", err)
}
} else {
// There is no history available
log.Trace("No history file available for PR %d", pull.Index)
}
}
if commitSHA != "" {
// Get immediate parent of the first commit in the patch, grab history back
parentCommit, _, err = git.NewCommand("rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(ctx, &git.RunOpts{Dir: ctx.Repo.GitRepo.Path})
if err == nil {
parentCommit = strings.TrimSpace(parentCommit)
}
// Special case on Git < 2.25 that doesn't fail on immediate empty history
if err != nil || parentCommit == "" {
log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
// bring at least partial history if it can work
parentCommit = commitSHA
}
}
baseCommit = parentCommit
} else {
// Keep an empty history or original commit
baseCommit = pull.MergeBase
func GetMergedBaseCommitID(ctx *context.Context, pull *issues_model.PullRequest) string {
if pull.MergeBase != "" {
return pull.MergeBase
}
return baseCommit
return calcMergeBase(ctx, pull)
}
func preparePullViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
@ -271,7 +269,13 @@ func prepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue)
setMergeTarget(ctx, pull)
ctx.Data["HasMerged"] = true
baseCommit := GetMergedBaseCommitID(ctx, issue)
baseCommit := GetMergedBaseCommitID(ctx, pull)
if baseCommit == "" {
ctx.Data["BaseTarget"] = pull.BaseBranch
ctx.Data["NumCommits"] = 0
ctx.Data["NumFiles"] = 0
return nil // no merge base, do nothing
}
compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
baseCommit, pull.GetGitHeadRefName(), false, false)

View File

@ -13,8 +13,11 @@ import (
access_model "code.gitea.io/gitea/models/perm/access"
repo_model "code.gitea.io/gitea/models/repo"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/json"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/storage"
"code.gitea.io/gitea/modules/timeutil"
git_service "code.gitea.io/gitea/services/git"
notify_service "code.gitea.io/gitea/services/notify"
@ -133,9 +136,47 @@ func UpdateComment(ctx context.Context, c *issues_model.Comment, contentVersion
// DeleteComment deletes the comment
func DeleteComment(ctx context.Context, doer *user_model.User, comment *issues_model.Comment) error {
err := db.WithTx(ctx, func(ctx context.Context) error {
return issues_model.DeleteComment(ctx, comment)
// TODO: delete review if the comment is the last comment of the review
// TODO: delete comment attachments
if err := comment.LoadAttachments(ctx); err != nil {
return err
}
if err := issues_model.DeleteComment(ctx, comment); err != nil {
return err
}
// delete comment attachments
if _, err := repo_model.DeleteAttachments(ctx, comment.Attachments, true); err != nil {
return fmt.Errorf("delete attachments: %w", err)
}
if comment.ReviewID > 0 {
if err := comment.LoadIssue(ctx); err != nil {
return err
}
if err := comment.Issue.LoadRepo(ctx); err != nil {
return err
}
if err := comment.Issue.LoadPullRequest(ctx); err != nil {
return err
}
if err := git.RemoveRef(ctx, comment.Issue.Repo.RepoPath(), issues_model.GetCodeCommentRef(comment.Issue.PullRequest.Index, comment.ID)); err != nil {
log.Error("Unable to remove ref in base repository for PR[%d] Error: %v", comment.Issue.PullRequest.ID, err)
// We should not return error here, because the comment has been removed from database.
// users have to delete this ref manually or we should have a synchronize between
// database comment table and git refs.
}
}
for _, attachment := range comment.Attachments {
if err := storage.Attachments.Delete(repo_model.AttachmentRelativePath(attachment.UUID)); err != nil {
// Even delete files failed, but the attachments has been removed from database, so we
// should not return error but only record the error on logs.
// users have to delete this attachments manually or we should have a
// synchronize between database attachment table and attachment storage
log.Error("delete attachment[uuid: %s] failed: %v", attachment.UUID, err)
}
}
return nil
})
if err != nil {
return err

View File

@ -200,7 +200,7 @@ func DeleteIssue(ctx context.Context, doer *user_model.User, gitRepo *git.Reposi
// delete pull request related git data
if issue.IsPull && gitRepo != nil {
if err := gitRepo.RemoveReference(issue.PullRequest.GetGitHeadRefName()); err != nil {
if err := git.RemoveRef(ctx, gitRepo.Path, issue.PullRequest.GetGitHeadRefName()); err != nil {
return err
}
}
@ -301,6 +301,8 @@ func deleteIssue(ctx context.Context, issue *issues_model.Issue) ([]string, erro
attachmentPaths = append(attachmentPaths, issue.Attachments[i].RelativePath())
}
// TODO: deference all review comments
// delete all database data still assigned to this issue
if err := db.DeleteBeans(ctx,
&issues_model.ContentHistory{IssueID: issue.ID},

View File

@ -184,7 +184,7 @@ func NewPullRequest(ctx context.Context, opts *NewPullRequestOptions) error {
return nil
}); err != nil {
// cleanup: this will only remove the reference, the real commit will be clean up when next GC
if err1 := baseGitRepo.RemoveReference(pr.GetGitHeadRefName()); err1 != nil {
if err1 := git.RemoveRef(ctx, baseGitRepo.Path, pr.GetGitHeadRefName()); err1 != nil {
log.Error("RemoveReference: %v", err1)
}
return err
@ -648,7 +648,7 @@ func UpdateRef(ctx context.Context, pr *issues_model.PullRequest) (err error) {
return err
}
_, _, err = git.NewCommand("update-ref").AddDynamicArguments(pr.GetGitHeadRefName(), pr.HeadCommitID).RunStdString(ctx, &git.RunOpts{Dir: pr.BaseRepo.RepoPath()})
err = git.UpdateRef(ctx, pr.BaseRepo.RepoPath(), pr.GetGitHeadRefName(), pr.HeadCommitID)
if err != nil {
log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
}

View File

@ -21,6 +21,7 @@ import (
"code.gitea.io/gitea/modules/cache"
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/gitrepo"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/util"
@ -141,11 +142,8 @@ func CreateCodeComment(ctx context.Context, doer *user_model.User, gitRepo *git.
if err != nil {
return nil, fmt.Errorf("GetRefCommitID[%s]: %w", issue.PullRequest.GetGitHeadRefName(), err)
}
} else {
// afterCommitID must be one of the pull request commits
if !slices.Contains(prCommitIDs, afterCommitID) {
return nil, fmt.Errorf("afterCommitID[%s] is not a valid pull request commit", afterCommitID)
}
} else if !slices.Contains(prCommitIDs, afterCommitID) { // afterCommitID must be one of the pull request commits
return nil, fmt.Errorf("afterCommitID[%s] is not a valid pull request commit", afterCommitID)
}
// CreateCodeComment() is used for:
@ -279,22 +277,33 @@ func createCodeComment(ctx context.Context, doer *user_model.User, repo *repo_mo
}
lineCommitID := util.Iif(line < 0, beforeCommitID, afterCommitID)
// TODO: the commit ID Must be referenced in the git repository, because the branch maybe rebased or force-pushed.
return db.WithTx2(ctx, func(ctx context.Context) (*issues_model.Comment, error) {
comment, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeCode,
Doer: doer,
Repo: repo,
Issue: issue,
Content: content,
LineNum: line,
TreePath: treePath,
CommitSHA: lineCommitID,
ReviewID: reviewID,
Patch: patch,
Invalidated: false,
Attachments: attachments,
})
if err != nil {
return nil, err
}
// If the commit ID is not referenced, it cannot be calculated the position dynamically.
return issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{
Type: issues_model.CommentTypeCode,
Doer: doer,
Repo: repo,
Issue: issue,
Content: content,
LineNum: line,
TreePath: treePath,
CommitSHA: lineCommitID,
ReviewID: reviewID,
Patch: patch,
Invalidated: false,
Attachments: attachments,
// The line commit ID Must be referenced in the git repository, because the branch maybe rebased or force-pushed.
// If the review commit is GC, the position will not be calculated dynamically.
if err := git.UpdateRef(ctx, pr.BaseRepo.RepoPath(), issues_model.GetCodeCommentRef(pr.Index, comment.ID), lineCommitID); err != nil {
log.Error("Unable to update ref in base repository for PR[%d] Error: %v", pr.ID, err)
return nil, err
}
return comment, nil
})
}

View File

@ -120,6 +120,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
if err = issues_model.DeleteComment(ctx, comment); err != nil {
return err
}
// TODO: delete attachments
}
}