in internal/git/localrepo/rebase.go [85:207]
func (repo *Repo) rebaseUsingMergeTree(ctx context.Context, cfg rebaseConfig, upstreamOID, branchOID git.ObjectID) (git.ObjectID, error) {
objectHash, err := repo.ObjectHash(ctx)
if err != nil {
return "", structerr.NewInternal("getting object hash %w", err)
}
// Flags of git-rev-list to get the pick-only todo_list for a rebase.
// Currently we drop clean cherry-picks and merge commits, which is
// also what git2go does.
// The flags are inferred from https://github.com/git/git/blob/v2.41.0/sequencer.c#L5704-L5714
flags := []gitcmd.Option{
gitcmd.Flag{Name: "--cherry-pick"},
gitcmd.Flag{Name: "--right-only"},
gitcmd.Flag{Name: "--no-merges"},
gitcmd.Flag{Name: "--topo-order"},
gitcmd.Flag{Name: "--reverse"},
}
var stderr bytes.Buffer
cmd, err := repo.Exec(ctx, gitcmd.Command{
Name: "rev-list",
Flags: flags,
// The notation "<upstream>...<branch>" is used to calculate the symmetric
// difference between upstream and branch. It will return the commits that
// are reachable exclusively from either side but not both. Combined with
// the provided --right-only flag, the result should be only commits which
// exist on the branch that is to be rebased.
Args: []string{fmt.Sprintf("%s...%s", upstreamOID, branchOID)},
}, gitcmd.WithStderr(&stderr), gitcmd.WithSetupStdout())
if err != nil {
return "", structerr.NewInternal("start git rev-list: %w", err)
}
var todoList []string
scanner := bufio.NewScanner(cmd)
for scanner.Scan() {
todoList = append(todoList, strings.TrimSpace(scanner.Text()))
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("scanning rev-list output: %w", err)
}
if err := cmd.Wait(); err != nil {
return "", structerr.NewInternal("git rev-list: %w", err).WithMetadata("stderr", stderr.String())
}
upstreamCommit, err := repo.ReadCommit(ctx, git.Revision(upstreamOID))
if err != nil {
return "", fmt.Errorf("reading upstream commit: %w", err)
}
oursCommitOID := upstreamOID
oursTreeOID := git.ObjectID(upstreamCommit.GetTreeId())
for _, todoItem := range todoList {
theirsCommit, err := repo.ReadCommit(ctx, git.Revision(todoItem))
if err != nil {
return "", fmt.Errorf("reading todo list commit: %w", err)
}
opts := []MergeTreeOption{WithAllowUnrelatedHistories()}
if len(theirsCommit.GetParentIds()) > 0 {
opts = append(opts, WithMergeBase(git.Revision(theirsCommit.GetParentIds()[0])))
}
newTreeOID, err := repo.MergeTree(ctx, oursCommitOID.String(), theirsCommit.GetId(), opts...)
if err != nil {
var conflictErr *MergeTreeConflictError
if errors.As(err, &conflictErr) {
return newTreeOID, &RebaseConflictError{
Commit: theirsCommit.GetId(),
ConflictError: conflictErr,
}
}
return "", fmt.Errorf("merging todo list commit: %w", err)
}
// When no tree changes detected, we need to further check
// 1. if the commit itself introduces no changes, pick it anyway.
// 2. if the commit is not empty to start and is not clean cherry-picks of any
// upstream commit, but become empty after rebasing, we just ignore it.
// Refer to https://git-scm.com/docs/git-rebase#Documentation/git-rebase.txt---emptydropkeepask
if newTreeOID == oursTreeOID {
if len(theirsCommit.GetParentIds()) == 0 {
if theirsCommit.GetTreeId() != objectHash.EmptyTreeOID.String() {
continue
}
} else {
theirsParentCommit, err := repo.ReadCommit(ctx, git.Revision(theirsCommit.GetParentIds()[0]))
if err != nil {
return "", fmt.Errorf("reading parent commit: %w", err)
}
if theirsCommit.GetTreeId() != theirsParentCommit.GetTreeId() {
continue
}
}
}
author := getSignatureFromCommitAuthor(theirsCommit.GetAuthor())
committer := cfg.committer
if committer == nil {
committer = getSignatureFromCommitAuthor(theirsCommit.GetCommitter())
}
newCommitOID, err := repo.WriteCommit(ctx, WriteCommitConfig{
Parents: []git.ObjectID{oursCommitOID},
AuthorName: author.Name,
AuthorEmail: author.Email,
AuthorDate: author.When,
CommitterName: committer.Name,
CommitterEmail: committer.Email,
CommitterDate: committer.When,
Message: string(theirsCommit.GetBody()),
TreeID: newTreeOID,
})
if err != nil {
return "", fmt.Errorf("write commit: %w", err)
}
oursCommitOID = newCommitOID
oursTreeOID = newTreeOID
}
return oursCommitOID, nil
}