diff --git a/git-imerge b/git-imerge index 0dca28c..4a14349 100755 --- a/git-imerge +++ b/git-imerge @@ -139,8 +139,6 @@ class Failure(Exception): return wrapper - pass - class AnsiColor: BLACK = '\033[0;30m' @@ -578,6 +576,23 @@ class TemporaryHead(object): return False +def is_ff(refname, commit): + """Would updating refname to commit be a fast-forward update? + + Return True iff refname is not currently set or it points to an + ancestor of commit. + + """ + + try: + ref_oldval = get_commit_sha1(refname) + except ValueError: + # refname doesn't already exist; no problem. + return True + else: + return MergeState._is_ancestor(ref_oldval, commit) + + def reparent(commit, parent_sha1s, msg=None): """Create a new commit object like commit, but with the specified parents. @@ -627,6 +642,53 @@ def reparent(commit, parent_sha1s, msg=None): return out.strip() +def create_commit_chain(base, path): + """Point refname at the chain of commits indicated by path. + + path is a list [(commit, metadata), ...]. Create a series of + commits corresponding to the entries in path. Each commit's tree + is taken from the corresponding old commit, and each commit's + metadata is taken from the corresponding metadata commit. Use base + as the parent of the first commit, or make the first commit a root + commit if base is None. Reuse existing commits from the list + whenever possible. + + Return a commit object corresponding to the last commit in the + chain. + + """ + + reusing = True + if base is None: + if not path: + raise ValueError('neither base nor path specified') + parents = [] + else: + parents = [base] + + for (commit, metadata) in path: + if reusing: + if commit == metadata and get_commit_parents(commit) == parents: + # We can reuse this commit, too. + parents = [commit] + continue + else: + reusing = False + + # Create a commit, copying the old log message and author info + # from the metadata commit: + tree = get_tree(commit) + new_commit = commit_tree( + tree, parents, + msg=get_log_message(metadata), + metadata=get_author_info(metadata), + ) + parents = [new_commit] + + [commit] = parents + return commit + + class AutomaticMergeFailed(Exception): def __init__(self, commit1, commit2): Exception.__init__( @@ -1847,6 +1909,13 @@ class SubBlock(Block): ) +class MissingMergeFailure(Failure): + def __init__(self, i1, i2): + Failure.__init__(self, 'Merge %d-%d is not yet done' % (i1, i2)) + self.i1 = i1 + self.i2 = i2 + + class MergeState(Block): SOURCE_TABLE = { 'auto': MergeRecord.SAVED_AUTO, @@ -2404,46 +2473,62 @@ class MergeState(Block): self._set_refname(refname, commit, force=force) - def simplify_to_rebase(self, refname, force=False): - i1 = self.len1 - 1 - for i2 in range(1, self.len2): - if not (i1, i2) in self: - raise Failure( - 'Cannot simplify to rebase because merge %d-%d is not yet done' - % (i1, i2) - ) + def _simplify_to_path(self, refname, base, path, force=False): + """Simplify based on path and set refname to the result. + + The base and path arguments are defined similarly to + create_commit_chain(), except that instead of SHA-1s they + represent commits via (i1, i2) tuples. + + """ + + base_sha1 = self[base].sha1 + path_sha1 = [] + for (commit, metadata) in path: + commit_record = self[commit] + if not commit_record.is_known(): + raise MissingMergeFailure(*commit) + metadata_record = self[metadata] + if not metadata_record.is_known(): + raise MissingMergeFailure(*metadata_record) + path_sha1.append((commit_record.sha1, metadata_record.sha1)) + + # A path simplification is allowed to discard history, as long + # as the *pre-simplification* apex commit is a descendant of + # the branch to be moved. + if path: + apex = path_sha1[-1][0] + else: + apex = base_sha1 - if not force: - # A rebase simplification is allowed to discard history, - # as long as the *pre-simplification* apex commit is a - # descendant of the branch to be moved. - try: - ref_oldval = get_commit_sha1(refname) - except ValueError: - # refname doesn't already exist; no problem. - pass - else: - commit = self[-1, -1].sha1 - if not MergeState._is_ancestor(ref_oldval, commit): - raise Failure( - '%s is not an ancestor of %s; use --force if you are sure' - % (commit, refname,) - ) + if not force and not is_ff(refname, apex): + raise Failure( + '%s cannot be updated to %s without discarding history.\n' + 'Use --force if you are sure, or choose a different reference' + % (refname, apex,) + ) - commit = self[i1, 0].sha1 - for i2 in range(1, self.len2): - orig = self[0, i2].sha1 - tree = get_tree(self[i1, i2].sha1) - authordata = get_author_info(orig) + # The update is OK, so here we can set force=True: + self._set_refname( + refname, + create_commit_chain(base_sha1, path_sha1), + force=True, + ) - # Create a commit, copying the old log message and author info: - commit = commit_tree( - tree, [commit], msg=get_log_message(orig), metadata=authordata, - ) + def simplify_to_rebase(self, refname, force=False): + i1 = self.len1 - 1 + path = [ + ((i1, i2), (0, i2)) + for i2 in range(1, self.len2) + ] - # We checked above that the update is OK, so here we can set - # force=True: - self._set_refname(refname, commit, force=True) + try: + self._simplify_to_path(refname, (i1, 0), path, force=force) + except MissingMergeFailure as e: + raise Failure( + 'Cannot simplify to rebase because merge %d-%d is not yet done' + % (e.i1, e.i2) + ) def simplify_to_merge(self, refname, force=False): if not (-1, -1) in self: