func()

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
}