From 3ed06ff361b8295d9aeede19a85dcb0ed12dcbfe Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Thu, 29 Oct 2015 14:57:11 +0100 Subject: [PATCH 1/4] Failure: remove superfluous line --- git-imerge | 2 -- 1 file changed, 2 deletions(-) diff --git a/git-imerge b/git-imerge index 0dca28c..e60d3eb 100755 --- a/git-imerge +++ b/git-imerge @@ -139,8 +139,6 @@ class Failure(Exception): return wrapper - pass - class AnsiColor: BLACK = '\033[0;30m' From 0e80a43fc69151c8c98a0fff804ae2ff926e3c96 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Thu, 29 Oct 2015 15:47:58 +0100 Subject: [PATCH 2/4] create_commit_chain(): new function Use the new function in the implementation of MergeState.simplify_to_rebase. --- git-imerge | 63 ++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 52 insertions(+), 11 deletions(-) diff --git a/git-imerge b/git-imerge index e60d3eb..0c636a8 100755 --- a/git-imerge +++ b/git-imerge @@ -625,6 +625,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__( @@ -2404,12 +2451,15 @@ class MergeState(Block): def simplify_to_rebase(self, refname, force=False): i1 = self.len1 - 1 + path = [] for i2 in range(1, self.len2): - if not (i1, i2) in self: + record = self[i1, i2] + if not record.is_known(): raise Failure( 'Cannot simplify to rebase because merge %d-%d is not yet done' % (i1, i2) ) + path.append((record.sha1, self[0, i2].sha1)) if not force: # A rebase simplification is allowed to discard history, @@ -2428,16 +2478,7 @@ class MergeState(Block): % (commit, refname,) ) - 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) - - # Create a commit, copying the old log message and author info: - commit = commit_tree( - tree, [commit], msg=get_log_message(orig), metadata=authordata, - ) + commit = create_commit_chain(self[i1, 0].sha1, path) # We checked above that the update is OK, so here we can set # force=True: From 502e1bbd61432a6ef19fef78d9d17bedeaf1a1ef Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Thu, 29 Oct 2015 17:16:48 +0100 Subject: [PATCH 3/4] is_ff(): new function Use the new function in the implementation of MergeState.simplify_to_rebase(). Also fix and improve an error message. --- git-imerge | 54 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 21 deletions(-) diff --git a/git-imerge b/git-imerge index 0c636a8..97ddb47 100755 --- a/git-imerge +++ b/git-imerge @@ -576,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. @@ -2461,28 +2478,23 @@ class MergeState(Block): ) path.append((record.sha1, self[0, i2].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,) - ) - - commit = create_commit_chain(self[i1, 0].sha1, path) + # 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. + apex = self[-1, -1].sha1 + 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,) + ) - # We checked above that the update is OK, so here we can set - # force=True: - self._set_refname(refname, commit, force=True) + # The update is OK, so here we can set force=True: + self._set_refname( + refname, + create_commit_chain(self[i1, 0].sha1, path), + force=True, + ) def simplify_to_merge(self, refname, force=False): if not (-1, -1) in self: From eeb518aa069fa295f220e116c5f3eb6adcf06531 Mon Sep 17 00:00:00 2001 From: Michael Haggerty Date: Thu, 29 Oct 2015 17:46:30 +0100 Subject: [PATCH 4/4] MergeState._simplify_to_path(): extract method Use the new function in the implementation of MergeState.simplify_to_rebase(). --- git-imerge | 66 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/git-imerge b/git-imerge index 97ddb47..4a14349 100755 --- a/git-imerge +++ b/git-imerge @@ -1909,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, @@ -2466,22 +2473,34 @@ class MergeState(Block): self._set_refname(refname, commit, force=force) - def simplify_to_rebase(self, refname, force=False): - i1 = self.len1 - 1 - path = [] - for i2 in range(1, self.len2): - record = self[i1, i2] - if not record.is_known(): - raise Failure( - 'Cannot simplify to rebase because merge %d-%d is not yet done' - % (i1, i2) - ) - path.append((record.sha1, self[0, i2].sha1)) + 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 - # 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. - apex = self[-1, -1].sha1 if not force and not is_ff(refname, apex): raise Failure( '%s cannot be updated to %s without discarding history.\n' @@ -2492,10 +2511,25 @@ class MergeState(Block): # The update is OK, so here we can set force=True: self._set_refname( refname, - create_commit_chain(self[i1, 0].sha1, path), + create_commit_chain(base_sha1, path_sha1), force=True, ) + def simplify_to_rebase(self, refname, force=False): + i1 = self.len1 - 1 + path = [ + ((i1, i2), (0, i2)) + for i2 in range(1, self.len2) + ] + + 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: raise Failure(