From 647e000e0d5021d7684ade725599ad78634f8ad4 Mon Sep 17 00:00:00 2001 From: Jesse Luehrs Date: Sun, 27 Oct 2013 23:54:08 -0400 Subject: update some things from their external sources --- bin/git/git-imerge | 729 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 499 insertions(+), 230 deletions(-) (limited to 'bin/git') diff --git a/bin/git/git-imerge b/bin/git/git-imerge index a9d646c..f2e7772 100755 --- a/bin/git/git-imerge +++ b/bin/git/git-imerge @@ -48,42 +48,14 @@ of an incremental merge can (crudely) be visualized using the An incremental merge can be interrupted and resumed arbitrarily, or even pushed to a server to allow somebody else to work on it. -When an incremental merge is finished, you can discard the -intermediate merge commits and create a simpler history to record -permanently in your project repository using either the "finish" or -"simplify" command. The incremental merge can be simplified in one of -three ways: - * merge - keep only a simple merge of the second branch into the first - branch, discarding all intermediate merges. The result is - similar to what you would get from - - git checkout BRANCH1 - git merge BRANCH2 - - * rebase - keep the versions of the commits from the second branch rebased - onto the first branch. The result is similar to what you would - get from - - git checkout BRANCH2 - git rebase BRANCH1 - - * rebase-with-history - like rebase, except that each of the rebased commits is recorded - as a merge from its original version to its rebased predecessor. - This is a style of rebasing that does not discard the old - version of the branch, and allows an already-published branch to - be rebased. See [3] for more information. - -Simple operation: +Instructions: For basic operation, you only need to know three git-imerge commands. -To merge BRANCH into MASTER or rebase BRANCH onto MASTER, +To merge BRANCH2 into BRANCH1 or rebase BRANCH2 onto BRANCH1, - git checkout MASTER - git-imerge start --name=NAME --goal=GOAL --first-parent BRANCH + git checkout BRANCH1 + git-imerge start --name=NAME --goal=GOAL --first-parent BRANCH2 while not done: git commit @@ -93,30 +65,60 @@ To merge BRANCH into MASTER or rebase BRANCH onto MASTER, where NAME is the name for this merge (and also the default name of the - branch to which the results will be saved) + branch to which the results will be saved). + + GOAL describes how you want the "finish" or "simplify" command to + record the results of the incremental merge in your project's + permanent history. The "goal" of the incremental merge can be + one of the following: + + * merge + keep only a simple merge of the second branch into the + first branch, discarding all intermediate merges. The end + result is similar to what you would get from + + git checkout BRANCH1 + git merge BRANCH2 + + * rebase + keep the versions of the commits from the second branch + rebased onto the first branch. The end result is similar + to what you would get from - GOAL describes how you want to simplify the results: + git checkout BRANCH2 + git rebase BRANCH1 - "merge" for a simple merge + * rebase-with-history + like rebase, except that it retains the old versions of + the rebased commits in the history. It is equivalent to + merging the commits from BRANCH2 into BRANCH1, one commit + at a time. In other words, it transforms this: - "rebase" for a simple rebase + o---o---o---o BRANCH1 + \ + A---B---C---D BRANCH2 - "rebase-with-history" for a rebase that retains history. This - is equivalent to merging the commits from BRANCH into MASTER, - one commit at a time. In other words, it transforms this:: + into this: - o---o---o---o MASTER - \ - A---B---C---D BRANCH + o---o---o---o---A'--B'--C'--D' NEW_BRANCH + \ / / / / + --------A---B---C---D - into this:: + It is safe to rebase an already-published branch using this + approach. See [3] for more information. - o---o---o---o---A'--B'--C'--D' MASTER - \ / / / / - --------A---B---C---D BRANCH + * full + don't simplify the incremental merge at all: do all of the + intermediate merges and retain them all in the permanent + history. - This is like a rebase, except that it retains the history of - individual merges. See [3] for more information. +For the full documentation, type + + git-imerge --help + +and + + git-imerge SUBCOMMAND --help [1] http://softwareswirl.blogspot.com/2013/05/git-imerge-practical-introduction.html @@ -190,12 +192,12 @@ except ImportError: return output -STATE_VERSION = (1, 0, 0) +STATE_VERSION = (1, 2, 0) ZEROS = '0' * 40 ALLOWED_GOALS = [ - #'full', + 'full', 'rebase-with-history', 'rebase', 'merge', @@ -428,7 +430,7 @@ def get_log_message(commit): def get_author_info(commit): a = check_output([ 'git', '--no-pager', 'log', '-n1', - '--format=%an%x00%ae%x00%ad', commit + '--format=%an%x00%ae%x00%ai', commit ]).strip().split('\x00') return { @@ -509,19 +511,22 @@ class TemporaryHead(object): return False -def reparent(commit, parent_sha1s): +def reparent(commit, parent_sha1s, msg=None): """Create a new commit object like commit, but with the specified parents. commit is the SHA1 of an existing commit and parent_sha1s is a list of SHA1s. Create a new commit exactly like that one, except that it has the specified parent commits. Return the SHA1 of the resulting commit object, which is already stored in the object - database but is not yet referenced by anything.""" + database but is not yet referenced by anything. + + If msg is set, then use it as the commit message for the new + commit.""" old_commit = check_output(['git', 'cat-file', 'commit', commit]) separator = old_commit.index('\n\n') headers = old_commit[:separator + 1].splitlines(True) - rest = old_commit[separator + 1:] + rest = old_commit[separator + 2:] new_commit = StringIO() for i in range(len(headers)): @@ -536,7 +541,13 @@ def reparent(commit, parent_sha1s): else: new_commit.write(line) - new_commit.write(rest) + new_commit.write('\n') + if msg is None: + new_commit.write(rest) + else: + new_commit.write(msg) + if not msg.endswith('\n'): + new_commit.write('\n') process = subprocess.Popen( ['git', 'hash-object', '-t', 'commit', '-w', '--stdin'], @@ -557,7 +568,7 @@ class AutomaticMergeFailed(Exception): self.commit1, self.commit2 = commit1, commit2 -def automerge(commit1, commit2): +def automerge(commit1, commit2, msg=None): """Attempt an automatic merge of commit1 and commit2. Return the SHA1 of the resulting commit, or raise @@ -565,8 +576,12 @@ def automerge(commit1, commit2): worktree.""" call_silently(['git', 'checkout', '-f', commit1]) + cmd = ['git', '-c', 'rerere.enabled=false', 'merge'] + if msg is not None: + cmd += ['-m', msg] + cmd += [commit2] try: - call_silently(['git', '-c', 'rerere.enabled=false', 'merge', commit2]) + call_silently(cmd) except CalledProcessError: # We don't use "git merge --abort" here because it was only # added in git version 1.7.4. @@ -682,8 +697,8 @@ class MergeRecord(object): def clear_ref(source): check_call([ 'git', 'update-ref', - '-d', 'imerge %r: Remove obsolete %s merge' % (name, source,), - 'refs/imerge/%s/%s/%d-%d' % (name, source, i1, i2), + '-m', 'imerge %r: Remove obsolete %s merge' % (name, source,), + '-d', 'refs/imerge/%s/%s/%d-%d' % (name, source, i1, i2), ]) if self.flags & self.MANUAL: @@ -1016,7 +1031,7 @@ class MergeFrontier(object): return AnsiColor.B_RED + node + AnsiColor.END if legend is None: - legend = ["?", "*", ".", "#", "@", "-", "|", "+"] + legend = ['?', '*', '.', '#', '@', '-', '|', '+'] merge = node & ~cls.FRONTIER_MASK within = merge == Block.MERGE_MANUAL or (node & cls.FRONTIER_WITHIN) skip = [Block.MERGE_MANUAL, Block.MERGE_BLOCKED, Block.MERGE_UNBLOCKED] @@ -1071,16 +1086,16 @@ class MergeFrontier(object): f.write(diagram[i1][i2]) f.write('\n') - def write_html(self, f, name, cssfile="imerge.css", abbrev_sha1=7): + def write_html(self, f, name, cssfile='imerge.css', abbrev_sha1=7): class_map = { - Block.MERGE_UNKNOWN: "merge_unknown", - Block.MERGE_MANUAL: "merge_manual", - Block.MERGE_AUTOMATIC: "merge_automatic", - Block.MERGE_BLOCKED: "merge_blocked", - Block.MERGE_UNBLOCKED: "merge_unblocked", - self.FRONTIER_WITHIN: "frontier_within", - self.FRONTIER_RIGHT_EDGE: "frontier_right_edge", - self.FRONTIER_BOTTOM_EDGE: "frontier_bottom_edge", + Block.MERGE_UNKNOWN: 'merge_unknown', + Block.MERGE_MANUAL: 'merge_manual', + Block.MERGE_AUTOMATIC: 'merge_automatic', + Block.MERGE_BLOCKED: 'merge_blocked', + Block.MERGE_UNBLOCKED: 'merge_unblocked', + self.FRONTIER_WITHIN: 'frontier_within', + self.FRONTIER_RIGHT_EDGE: 'frontier_right_edge', + self.FRONTIER_BOTTOM_EDGE: 'frontier_bottom_edge', } def map_to_classes(node): @@ -1104,17 +1119,17 @@ class MergeFrontier(object): diagram = self.create_diagram() for i2 in range(self.block.len2): - f.write(" \n") + f.write(' \n') for i1 in range(self.block.len1): classes = map_to_classes(diagram[i1][i2]) record = self.block.get_value(i1, i2) - sha1 = record.sha1 or "" + sha1 = record.sha1 or '' td_id = record.sha1 and ' id="%s"' % (record.sha1) or '' - td_class = classes and ' class="' + " ".join(classes) + '"' or '' - f.write(" %.*s\n" % ( + td_class = classes and ' class="' + ' '.join(classes) + '"' or '' + f.write(' %.*s\n' % ( td_id, td_class, abbrev_sha1, sha1)) - f.write(" \n") - f.write("\n\n\n") + f.write(' \n') + f.write('\n\n\n') @staticmethod def _iter_non_empty_blocks(blocks): @@ -1255,18 +1270,20 @@ class MergeFrontier(object): blocks = list(self.iter_blocker_blocks()) if not blocks: raise BlockCompleteError('The block is already complete') - # Try blocks from biggest to smallest: - blocks.sort(key=lambda block: block.get_area(), reverse=True) + + # Try blocks from left to right: + blocks.sort(key=lambda block: block.get_original_indexes(0, 0)) + for block in blocks: - if block.auto_outline_frontier(): + if block.auto_expand_frontier(): return else: # None of the blocks could be expanded. Suggest that the # caller do a manual merge of the commit that is blocking - # the *smallest* blocker block. - i1, i2 = blocks[-1].get_original_indexes(1, 1) + # the leftmost blocker block. + i1, i2 = blocks[0].get_original_indexes(1, 1) raise FrontierBlockedError( - 'Frontier blocked; suggest manual merge of %d-%d' % (i1, i2), + 'Conflict; suggest manual merge of %d-%d' % (i1, i2), i1, i2 ) @@ -1299,14 +1316,22 @@ class Block(object): Members: + name -- the name of the imerge of which this block is part. + len1, len2 -- the dimensions of the block. """ - def __init__(self, len1, len2): + def __init__(self, name, len1, len2): + self.name = name self.len1 = len1 self.len2 = len2 + def get_merge_state(self): + """Return the MergeState instance containing this Block.""" + + raise NotImplementedError() + def get_area(self): """Return the area of this block, ignoring the known edges.""" @@ -1420,9 +1445,11 @@ class Block(object): def do_merge(i1, commit1, i2, commit2, msg='Autofilling %d-%d...', record=True): if (i1, i2) in self: return self[i1,i2].sha1 + (i1orig, i2orig) = self.get_original_indexes(i1, i2) + sys.stderr.write(msg % (i1orig, i2orig)) + logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) try: - sys.stderr.write(msg % self.get_original_indexes(i1, i2)) - merge = automerge(commit1, commit2) + merge = automerge(commit1, commit2, msg=logmsg) sys.stderr.write('success.\n') except AutomaticMergeFailed, e: sys.stderr.write('unexpected conflict. Backtracking...\n') @@ -1441,42 +1468,75 @@ class Block(object): for i2 in range(1, self.len2 - 1): above = do_merge(i1, above, i2, self[0,i2].sha1) - # We will compare two ways of doing the final "vertex" merge: - # as a continuation of the bottom edge, or as a continuation - # of the right edge. We only accept it if both approaches - # succeed and give identical trees. i1, i2 = self.len1 - 1, self.len2 - 1 - vertex_v1 = do_merge( - i1, self[i1,0].sha1, i2, left, - msg='Autofilling %d-%d (first way)...', - record=False, - ) - vertex_v2 = do_merge( - i1, above, i2, self[0,i2].sha1, - msg='Autofilling %d-%d (second way)...', - record=False, - ) - if get_tree(vertex_v1) == get_tree(vertex_v2): - sys.stderr.write( - 'The two ways of autofilling %d-%d agree.\n' - % self.get_original_indexes(i1, i2) + if i1 > 1 and i2 > 1: + # We will compare two ways of doing the final "vertex" merge: + # as a continuation of the bottom edge, or as a continuation + # of the right edge. We only accept it if both approaches + # succeed and give identical trees. + vertex_v1 = do_merge( + i1, self[i1,0].sha1, i2, left, + msg='Autofilling %d-%d (first way)...', + record=False, ) + vertex_v2 = do_merge( + i1, above, i2, self[0,i2].sha1, + msg='Autofilling %d-%d (second way)...', + record=False, + ) + if get_tree(vertex_v1) == get_tree(vertex_v2): + sys.stderr.write( + 'The two ways of autofilling %d-%d agree.\n' + % self.get_original_indexes(i1, i2) + ) + # Everything is OK. Now reparent the actual vertex merge to + # have above and left as its parents: + merges.append((i1, i2, reparent(vertex_v1, [above, left]))) + else: + sys.stderr.write( + 'The two ways of autofilling %d-%d do not agree. Backtracking...\n' + % self.get_original_indexes(i1, i2) + ) + raise UnexpectedMergeFailure('Inconsistent vertex merges', i1, i2) else: - sys.stderr.write( - 'The two ways of autofilling %d-%d do not agree. Backtracking...\n' - % self.get_original_indexes(i1, i2) + do_merge( + i1, above, i2, left, + msg='Autofilling %d-%d...', ) - raise UnexpectedMergeFailure('Inconsistent vertex merges', i1, i2) - - # Everything is OK. Now reparent the actual vertex merge to - # have above and left as its parents: - merges.append((i1, i2, reparent(vertex_v1, [above, left]))) # Done! Now we can record the results: sys.stderr.write('Recording autofilled block %s.\n' % (self,)) for (i1, i2, merge) in merges: self[i1, i2].record_merge(merge, MergeRecord.NEW_AUTO) + def auto_fill_micromerge(self): + """Try to fill the very first micromerge in this block. + + Return True iff the attempt was successful.""" + + assert (1, 1) not in self + if self.len1 <= 1 or self.len2 <= 1 or self.is_blocked(1, 1): + return False + + i1, i2 = 1, 1 + (i1orig, i2orig) = self.get_original_indexes(i1, i2) + sys.stderr.write('Attempting to merge %d-%d...' % (i1orig, i2orig)) + logmsg = 'imerge \'%s\': automatic merge %d-%d' % (self.name, i1orig, i2orig) + try: + merge = automerge( + self[i1, i2 - 1].sha1, + self[i1 - 1, i2].sha1, + msg=logmsg, + ) + sys.stderr.write('success.\n') + except AutomaticMergeFailed, e: + sys.stderr.write('conflict.\n') + self[i1,i2].record_blocked(True) + return False + else: + self[i1, i2].record_merge(merge, MergeRecord.NEW_AUTO) + return True + def auto_outline_frontier(self, merge_frontier=None): """Try to outline the merge frontier of this block. @@ -1489,7 +1549,7 @@ class Block(object): # Nothing to do. return False - best_block = max(merge_frontier, key=lambda block: block.get_area()) + best_block = max(merge_frontier, key=lambda block: block.get_original_indexes(0, 0)) try: best_block.auto_outline() @@ -1506,6 +1566,15 @@ class Block(object): f2.block.auto_outline_frontier(f2) return True + def auto_expand_frontier(self): + merge_state = self.get_merge_state() + if merge_state.manual: + return False + elif merge_state.goal == 'full': + return self.auto_fill_micromerge() + else: + return self.auto_outline_frontier() + # The codes in the 2D array returned from create_diagram() MERGE_UNKNOWN = 0 MERGE_MANUAL = 1 @@ -1545,11 +1614,11 @@ class Block(object): def format_diagram(self, legend=None, diagram=None): if legend is None: legend = [ - AnsiColor.D_GRAY + "?" + AnsiColor.END, - AnsiColor.B_GREEN + "*" + AnsiColor.END, - AnsiColor.B_GREEN + "." + AnsiColor.END, - AnsiColor.B_RED + "#" + AnsiColor.END, - AnsiColor.B_YELLOW + "@" + AnsiColor.END, + AnsiColor.D_GRAY + '?' + AnsiColor.END, + AnsiColor.B_GREEN + '*' + AnsiColor.END, + AnsiColor.B_GREEN + '.' + AnsiColor.END, + AnsiColor.B_RED + '#' + AnsiColor.END, + AnsiColor.B_YELLOW + '@' + AnsiColor.END, ] if diagram is None: diagram = self.create_diagram() @@ -1592,23 +1661,30 @@ class SubBlock(Block): def __init__(self, block, slice1, slice2): (start1, len1) = self._convert_to_slice(slice1, block.len1) (start2, len2) = self._convert_to_slice(slice2, block.len2) - Block.__init__(self, len1, len2) + Block.__init__(self, block.name, len1, len2) if isinstance(block, SubBlock): # Peel away one level of indirection: - self._block = block._block + self._merge_state = block._merge_state self._start1 = start1 + block._start1 self._start2 = start2 + block._start2 else: - self._block = block + assert(isinstance(block, MergeState)) + self._merge_state = block self._start1 = start1 self._start2 = start2 + def get_merge_state(self): + return self._merge_state + def get_original_indexes(self, i1, i2): i1, i2 = self._normalize_indexes((i1, i2)) - return self._block.get_original_indexes(i1 + self._start1, i2 + self._start2) + return self._merge_state.get_original_indexes( + i1 + self._start1, + i2 + self._start2, + ) def convert_original_indexes(self, i1, i2): - (i1, i2) = self._block.convert_original_indexes(i1, i2) + (i1, i2) = self._merge_state.convert_original_indexes(i1, i2) if not ( self._start1 <= i1 < self._start1 + self.len1 and self._start2 <= i2 < self._start2 + self.len2 @@ -1618,15 +1694,19 @@ class SubBlock(Block): def _set_value(self, i1, i2, sha1, flags): self._check_indexes(i1, i2) - self._block._set_value(i1 + self._start1, i2 + self._start2, sha1, flags) + self._merge_state._set_value( + i1 + self._start1, + i2 + self._start2, + sha1, flags, + ) def get_value(self, i1, i2): self._check_indexes(i1, i2) - return self._block.get_value(i1 + self._start1, i2 + self._start2) + return self._merge_state.get_value(i1 + self._start1, i2 + self._start2) def __str__(self): return '%s[%d:%d,%d:%d]' % ( - self._block, + self._merge_state, self._start1, self._start1 + self.len1, self._start2, self._start2 + self.len2, ) @@ -1670,7 +1750,7 @@ class MergeState(Block): state = json.loads(state_string) version = tuple(map(int, state['version'].split('.'))) - if version[0] != STATE_VERSION[0] or version > STATE_VERSION: + if version[0] != STATE_VERSION[0] or version[1] > STATE_VERSION[1]: raise Failure( 'The format of imerge %s (%s) is not compatible with this script version.' % (name, state['version'],) @@ -1683,7 +1763,7 @@ class MergeState(Block): """Verify that a MergeState with the given name exists. Just check for the existence, readability, and compatible - version of the "state" reference. If the reference doesn't + version of the 'state' reference. If the reference doesn't exist, just return False. If it exists but is unusable for some other reason, raise an exception.""" @@ -1747,8 +1827,8 @@ class MergeState(Block): ) @staticmethod - def initialize(name, goal, tip1, tip2): - """Create and return a new MergeState object.""" + def check_name_format(name): + """Check that name is a valid imerge name.""" if '/' in name: raise Failure('Name %r must not include a slash character!' % (name,)) @@ -1760,6 +1840,12 @@ class MergeState(Block): except CalledProcessError: raise Failure('Name %r is not a valid refname component!' % (name,)) + @staticmethod + def initialize(name, tip1, tip2, goal=DEFAULT_GOAL, manual=False): + """Create and return a new MergeState object.""" + + MergeState.check_name_format(name) + if check_output(['git', 'for-each-ref', 'refs/imerge/%s' % (name,)]): raise Failure('Name %r is already in use!' % (name,)) @@ -1801,7 +1887,14 @@ class MergeState(Block): if goal == 'rebase': MergeState._check_no_merges(commits2) - return MergeState(name, goal, merge_base, commits1, commits2, MergeRecord.NEW_MANUAL) + return MergeState( + name, merge_base, + tip1, commits1, + tip2, commits2, + MergeRecord.NEW_MANUAL, + goal=goal, + manual=manual, + ) @staticmethod def read(name): @@ -1863,13 +1956,9 @@ class MergeState(Block): if state is None: raise Failure( 'No state found; it should have been a blob reference at ' - '"refs/imerge/%s/state' % (name,) + '"refs/imerge/%s/state"' % (name,) ) - goal = state['goal'] - if goal not in ALLOWED_GOALS: - raise Failure('Goal %r, read from state, is not recognized.' % (goal,)) - blockers = state.get('blockers', []) if unexpected: @@ -1902,7 +1991,23 @@ class MergeState(Block): except KeyError: break - state = MergeState(name, goal, merge_base, commits1, commits2, MergeRecord.SAVED_MANUAL) + tip1 = state.get('tip1', commits1[-1]) + tip2 = state.get('tip2', commits2[-1]) + + goal = state['goal'] + if goal not in ALLOWED_GOALS: + raise Failure('Goal %r, read from state, is not recognized.' % (goal,)) + + manual = state['manual'] + + state = MergeState( + name, merge_base, + tip1, commits1, + tip2, commits2, + MergeRecord.SAVED_MANUAL, + goal=goal, + manual=manual, + ) # Now write the rest of the merges to state: for ((i1, i2), (sha1, source)) in merges.iteritems(): @@ -1973,10 +2078,19 @@ class MergeState(Block): if MergeState.get_default_name() == name: MergeState.set_default_name(None) - def __init__(self, name, goal, merge_base, commits1, commits2, source): - Block.__init__(self, len(commits1) + 1, len(commits2) + 1) - self.name = name + def __init__( + self, name, merge_base, + tip1, commits1, + tip2, commits2, + source, + goal=DEFAULT_GOAL, + manual=False, + ): + Block.__init__(self, name, len(commits1) + 1, len(commits2) + 1) + self.tip1 = tip1 + self.tip2 = tip2 self.goal = goal + self.manual = bool(manual) # A simulated 2D array. Values are None or MergeRecord instances. self._data = [[None] * self.len2 for i1 in range(self.len1)] @@ -1987,6 +2101,9 @@ class MergeState(Block): for (i2, commit) in enumerate(commits2, 1): self.get_value(0, i2).record_merge(commit, source) + def get_merge_state(self): + return self + def set_goal(self, goal): if goal not in ALLOWED_GOALS: raise ValueError('%r is not an allowed goal' % (goal,)) @@ -2079,7 +2196,7 @@ class MergeState(Block): swapped = False if i1first < i1second: # Swap parents to make the parent from above the first parent: - (i1first, i2first, i1second, i2second) = (i1second, i2second, i2first, i1first) + (i1first, i2first, i1second, i2second) = (i1second, i2second, i1first, i2first) swapped = True if i1first != i1second + 1 or i2first != i2second - 1: raise ManualMergeUnusableError( @@ -2140,6 +2257,18 @@ class MergeState(Block): ]) checkout(refname) + def simplify_to_full(self, refname, force=False): + for i1 in range(1, self.len1): + for i2 in range(1, self.len2): + if not (i1, i2) in self: + raise Failure( + 'Cannot simplify to "full" because ' + 'merge %d-%d is not yet done' + % (i1, i2) + ) + + self._set_refname(refname, self[-1, -1].sha1, force=force) + def simplify_to_rebase_with_history(self, refname, force=False): i1 = self.len1 - 1 for i2 in range(1, self.len2): @@ -2156,7 +2285,11 @@ class MergeState(Block): tree = get_tree(self[i1, i2].sha1) # Create a commit, copying the old log message: - commit = commit_tree(tree, [commit, orig], msg=get_log_message(orig)) + msg = ( + get_log_message(orig).rstrip('\n') + + '\n\n(rebased-with-history from commit %s)\n' % orig + ) + commit = commit_tree(tree, [commit, orig], msg=msg) self._set_refname(refname, commit, force=force) @@ -2169,6 +2302,23 @@ class MergeState(Block): % (i1, i2) ) + 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 = self[i1, 0].sha1 for i2 in range(1, self.len2): orig = self[0, i2].sha1 @@ -2180,7 +2330,9 @@ class MergeState(Block): tree, [commit], msg=get_log_message(orig), metadata=authordata, ) - self._set_refname(refname, commit, force=force) + # We checked above that the update is OK, so here we can set + # force=True: + self._set_refname(refname, commit, force=True) def simplify_to_merge(self, refname, force=False): if not (-1, -1) in self: @@ -2194,7 +2346,7 @@ class MergeState(Block): # Create a preliminary commit with a generic commit message: sha1 = commit_tree( tree, parents, - msg='Merge commit %s into commit %s' % (parents[1], parents[0]), + msg='Merge %s into %s (using imerge)' % (self.tip2, self.tip1), ) self._set_refname(refname, sha1, force=force) @@ -2207,7 +2359,9 @@ class MergeState(Block): The merge must be complete before calling this method.""" - if self.goal == 'rebase-with-history': + if self.goal == 'full': + self.simplify_to_full(refname, force=force) + elif self.goal == 'rebase-with-history': self.simplify_to_rebase_with_history(refname, force=force) elif self.goal == 'rebase': self.simplify_to_rebase(refname, force=force) @@ -2230,8 +2384,10 @@ class MergeState(Block): state = dict( version='.'.join(map(str, STATE_VERSION)), - goal=self.goal, blockers=blockers, + tip1=self.tip1, tip2=self.tip2, + goal=self.goal, + manual=self.manual, ) state_string = json.dumps(state, sort_keys=True) + '\n' @@ -2250,14 +2406,16 @@ class MergeState(Block): ]) def __str__(self): - return 'MergeState(%r, goal=%r)' % (self.name, self.goal,) + return 'MergeState(\'%s\', tip1=\'%s\', tip2=\'%s\', goal=\'%s\')' % ( + self.name, self.tip1, self.tip2, self.goal, + ) def request_user_merge(merge_state, i1, i2): """Prepare the working tree for the user to do a manual merge. - It is assumed that the merge above and to the left of (i1, i2) are - already done.""" + It is assumed that the merges above and to the left of (i1, i2) + are already done.""" above = merge_state[i1, i2 - 1] left = merge_state[i1 - 1, i2] @@ -2270,10 +2428,11 @@ def request_user_merge(merge_state, i1, i2): refname, above.sha1, ]) checkout(refname) + logmsg = 'imerge \'%s\': manual merge %d-%d' % (merge_state.name, i1, i2) try: check_call([ 'git', 'merge', '--no-commit', - '-m', 'Merge %d-%d of incremental merge \'%s\'' % (i1, i2, merge_state.name,), + '-m', logmsg, left.sha1, ]) except CalledProcessError: @@ -2298,39 +2457,110 @@ def request_user_merge(merge_state, i1, i2): % (i1, i2) ) -def incorporate_user_merge(merge_state): + +def incorporate_user_merge(merge_state, edit_log_msg=None): """If the user has done a merge for us, incorporate the results. - If reference refs/heads/imerge/NAME exists, try to incorporate it - into merge_state, delete the reference, and return (i1,i2) - corresponding to the merge. If the reference cannot be used, - raise NoManualMergeError(). If the reference exists but cannot be - used, raise a ManualMergeUnusableError. This function must be - called with a clean work tree.""" + If the scratch reference refs/heads/imerge/NAME exists and is + checked out, first check if there are staged changes that can be + committed. Then try to incorporate the current commit into + merge_state, delete the reference, and return (i1,i2) + corresponding to the merge. If the scratch reference does not + exist, raise NoManualMergeError(). If the scratch reference + exists but cannot be used, raise a ManualMergeUnusableError. If + there are unstaged changes in the working tree, emit an error + message and raise UncleanWorkTreeError.""" refname = MergeState.get_scratch_refname(merge_state.name) + try: commit = get_commit_sha1(refname) except ValueError: - raise NoManualMergeError('There was no merge at %s!' % (refname,)) + raise NoManualMergeError('Reference %s does not exist.' % (refname,)) + + try: + head_name = check_output(['git', 'symbolic-ref', '--quiet', 'HEAD']).strip() + except CalledProcessError: + raise NoManualMergeError('HEAD is currently detached.') + + if head_name != refname: + # This should not usually happen. The scratch reference + # exists, but it is not current. Perhaps the user gave up on + # an attempted merge then switched to another branch. We want + # to delete refname, but only if it doesn't contain any + # content that we don't already know. + try: + merge_state.find_index(commit) + except CommitNotFoundError: + # It points to a commit that we don't have in our records. + raise Failure( + 'The scratch reference, %(refname)s, already exists but is not\n' + 'checked out. If it points to a merge commit that you would like\n' + 'to use, please check it out using\n' + '\n' + ' git checkout %(refname)s\n' + '\n' + 'and then try to continue again. If it points to a commit that is\n' + 'unneeded, then please delete the reference using\n' + '\n' + ' git update-ref -d %(refname)s\n' + '\n' + 'and then continue.' + % dict(refname=refname) + ) + else: + # It points to a commit that is already recorded. We can + # delete it without losing any information. + check_call([ + 'git', 'update-ref', + '-m', + 'imerge %r: Remove obsolete scratch reference' + % (merge_state.name,), + '-d', refname, + ]) + sys.stderr.write( + '%s did not point to a new merge; it has been deleted.\n' + % (refname,) + ) + raise NoManualMergeError( + 'Reference %s was not checked out.' % (refname,) + ) + + # If we reach this point, then the scratch reference exists and is + # checked out. Now check whether there is staged content that + # can be committed: merge_frontier = MergeFrontier.map_known_frontier(merge_state) + try: + check_call(['git', 'diff-index', '--cached', '--quiet', 'HEAD', '--']) + except CalledProcessError: + # There are staged changes; commit them if possible. + cmd = ['git', 'commit', '--no-verify'] + + if edit_log_msg is not None: + if edit_log_msg: + cmd += ['--edit'] + else: + cmd += ['--no-edit'] + + try: + check_call(cmd) + except CalledProcessError: + raise Failure('Could not commit staged changes.') + commit = get_commit_sha1('HEAD') + + require_clean_work_tree('proceed') + # This might throw ManualMergeUnusableError: (i1, i2) = merge_state.incorporate_manual_merge(commit) - try: - headref = check_output(['git', 'symbolic-ref', '-q', 'HEAD']).strip() - except CalledProcessError: - pass - else: - if headref == refname: - # Detach head so that we can delete refname. - check_call([ - 'git', 'update-ref', '--no-deref', - '-m', 'Detach HEAD from %s' % (refname,), - 'HEAD', commit, - ]) + # Now detach head so that we can delete refname. + check_call([ + 'git', 'update-ref', '--no-deref', + '-m', 'Detach HEAD from %s' % (refname,), + 'HEAD', commit, + ]) check_call([ 'git', 'update-ref', @@ -2385,8 +2615,8 @@ def choose_merge_name(name, default_to_unique=True): raise Failure('Please select an incremental merge using --name') -def read_merge_state(name=None): - return MergeState.read(choose_merge_name(name)) +def read_merge_state(name=None, default_to_unique=True): + return MergeState.read(choose_merge_name(name, default_to_unique=default_to_unique)) @Failure.wrap @@ -2397,42 +2627,46 @@ def main(args): ) subparsers = parser.add_subparsers(dest='subcommand', help='sub-command') - parser_start = subparsers.add_parser( + subparser = subparsers.add_parser( 'start', help=( 'start a new incremental merge ' '(equivalent to "init" followed by "continue")' ), ) - parser_start.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name to use for this incremental merge', ) - parser_start.add_argument( + subparser.add_argument( '--goal', action='store', default=DEFAULT_GOAL, choices=ALLOWED_GOALS, help='the goal of the incremental merge', ) - #parser_start.add_argument( - # '--conflicts', ... - # action='store', default='pairwise', - # choices=['pairwise', 'fewest'], - # help='what sort of conflicts will be presented to the user', - # ) - parser_start.add_argument( + subparser.add_argument( + '--manual', + action='store_true', default=False, + help=( + 'ask the user to complete all merges manually, even when they ' + 'appear conflict-free. This option disables the usual bisection ' + 'algorithm and causes the full incremental merge diagram to be ' + 'completed.' + ), + ) + subparser.add_argument( '--first-parent', action='store_true', default=None, help=( 'handle only the first parent commits ' '(this option is currently required)' ), ) - parser_start.add_argument( + subparser.add_argument( 'branch', action='store', help='the tip of the branch to be merged into HEAD', ) - parser_continue = subparsers.add_parser( + subparser = subparsers.add_parser( 'continue', help=( 'record the merge at branch imerge/NAME ' @@ -2442,23 +2676,32 @@ def main(args): 'conflict that has to be resolved manually)' ), ) - parser_continue.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to continue', ) + subparser.set_defaults(edit=None) + subparser.add_argument( + '--edit', '-e', dest='edit', action='store_true', + help='commit staged changes with the --edit option', + ) + subparser.add_argument( + '--no-edit', dest='edit', action='store_false', + help='commit staged changes with the --no-edit option', + ) - parser_finish = subparsers.add_parser( + subparser = subparsers.add_parser( 'finish', help=( 'simplify then remove a completed incremental merge ' '(equivalent to "simplify" followed by "remove")' ), ) - parser_finish.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to finish', ) - parser_finish.add_argument( + subparser.add_argument( '--goal', action='store', default=None, choices=ALLOWED_GOALS, @@ -2467,7 +2710,7 @@ def main(args): '(default is the value provided to "init" or "start")' ), ) - parser_finish.add_argument( + subparser.add_argument( '--branch', action='store', default=None, help=( @@ -2478,42 +2721,42 @@ def main(args): 'specified.' ), ) - parser_finish.add_argument( + subparser.add_argument( '--force', action='store_true', default=False, help='allow the target branch to be updated in a non-fast-forward manner', ) - parser_diagram = subparsers.add_parser( + subparser = subparsers.add_parser( 'diagram', help='display a diagram of the current state of a merge', ) - parser_diagram.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to diagram', ) - parser_diagram.add_argument( + subparser.add_argument( '--commits', action='store_true', default=False, help='show the merges that have been made so far', ) - parser_diagram.add_argument( + subparser.add_argument( '--frontier', action='store_true', default=False, help='show the current merge frontier', ) - parser_diagram.add_argument( + subparser.add_argument( '--html', action='store', default=None, help='generate HTML diagram showing the current merge frontier', ) - parser_diagram.add_argument( + subparser.add_argument( '--color', dest='color', action='store_true', default=None, help='draw diagram with colors', ) - parser_diagram.add_argument( + subparser.add_argument( '--no-color', dest='color', action='store_false', help='draw diagram without colors', ) - parser_list = subparsers.add_parser( + subparser = subparsers.add_parser( 'list', help=( 'list the names of incremental merges that are currently in progress. ' @@ -2521,57 +2764,70 @@ def main(args): ), ) - parser_init = subparsers.add_parser( + subparser = subparsers.add_parser( 'init', help='initialize a new incremental merge', ) - parser_init.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name to use for this incremental merge', ) - parser_init.add_argument( + subparser.add_argument( '--goal', action='store', default=DEFAULT_GOAL, choices=ALLOWED_GOALS, help='the goal of the incremental merge', ) - #parser_init.add_argument( - # '--conflicts', ... - # action='store', default='pairwise', - # choices=['pairwise', 'fewest'], - # help='what sort of conflicts will be presented to the user', - # ) - parser_init.add_argument( + subparser.add_argument( + '--manual', + action='store_true', default=False, + help=( + 'ask the user to complete all merges manually, even when they ' + 'appear conflict-free. This option disables the usual bisection ' + 'algorithm and causes the full incremental merge diagram to be ' + 'completed.' + ), + ) + subparser.add_argument( '--first-parent', action='store_true', default=None, help=( 'handle only the first parent commits ' '(this option is currently required)' ), ) - parser_init.add_argument( + subparser.add_argument( 'branch', action='store', help='the tip of the branch to be merged into HEAD', ) - parser_record = subparsers.add_parser( + subparser = subparsers.add_parser( 'record', help='record the merge at branch imerge/NAME', ) - parser_record.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to which the merge should be added', ) + subparser.set_defaults(edit=None) + subparser.add_argument( + '--edit', '-e', dest='edit', action='store_true', + help='commit staged changes with the --edit option', + ) + subparser.add_argument( + '--no-edit', dest='edit', action='store_false', + help='commit staged changes with the --no-edit option', + ) - parser_autofill = subparsers.add_parser( + subparser = subparsers.add_parser( 'autofill', help='autofill non-conflicting merges', ) - parser_autofill.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to autofill', ) - parser_simplify = subparsers.add_parser( + subparser = subparsers.add_parser( 'simplify', help=( 'simplify a completed incremental merge by discarding unneeded ' @@ -2579,11 +2835,11 @@ def main(args): 'that are retained' ), ) - parser_simplify.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of merge to simplify', ) - parser_simplify.add_argument( + subparser.add_argument( '--goal', action='store', default=None, choices=ALLOWED_GOALS, @@ -2592,7 +2848,7 @@ def main(args): '(default is the value provided to "init" or "start")' ), ) - parser_simplify.add_argument( + subparser.add_argument( '--branch', action='store', default=None, help=( @@ -2603,26 +2859,26 @@ def main(args): 'specified.' ), ) - parser_simplify.add_argument( + subparser.add_argument( '--force', action='store_true', default=False, help='allow the target branch to be updated in a non-fast-forward manner', ) - parser_remove = subparsers.add_parser( + subparser = subparsers.add_parser( 'remove', help='irrevocably remove an incremental merge', ) - parser_remove.add_argument( + subparser.add_argument( '--name', action='store', default=None, help='name of incremental merge to remove', ) - parser_reparent = subparsers.add_parser( + subparser = subparsers.add_parser( 'reparent', help='change the parents of the HEAD commit', ) - parser_reparent.add_argument( + subparser.add_argument( 'parents', nargs='*', help='[PARENT...]', ) @@ -2653,8 +2909,16 @@ def main(args): parser.error( 'Please specify the --name to be used for this incremental merge' ) + try: + tip1 = check_output( + ['git', 'symbolic-ref', '--quiet', '--short', 'HEAD'], + ).strip() + except CalledProcessError: + tip1 = 'HEAD' + tip2 = options.branch merge_state = MergeState.initialize( - options.name, options.goal, 'HEAD', options.branch, + options.name, tip1, tip2, + goal=options.goal, manual=options.manual, ) merge_state.save() MergeState.set_default_name(options.name) @@ -2669,8 +2933,16 @@ def main(args): parser.error( 'Please specify the --name to be used for this incremental merge' ) + try: + tip1 = check_output( + ['git', 'symbolic-ref', '--quiet', '--short', 'HEAD'], + ).strip() + except CalledProcessError: + tip1 = 'HEAD' + tip2 = options.branch merge_state = MergeState.initialize( - options.name, options.goal, 'HEAD', options.branch, + options.name, tip1, tip2, + goal=options.goal, manual=options.manual, ) merge_state.save() MergeState.set_default_name(options.name) @@ -2684,10 +2956,9 @@ def main(args): elif options.subcommand == 'remove': MergeState.remove(choose_merge_name(options.name, default_to_unique=False)) elif options.subcommand == 'continue': - require_clean_work_tree('proceed') merge_state = read_merge_state(options.name) try: - incorporate_user_merge(merge_state) + incorporate_user_merge(merge_state, edit_log_msg=options.edit) except NoManualMergeError: pass except NotABlockingCommitError, e: @@ -2702,10 +2973,9 @@ def main(args): else: sys.stderr.write('Merge is complete!\n') elif options.subcommand == 'record': - require_clean_work_tree('proceed') merge_state = read_merge_state(options.name) try: - incorporate_user_merge(merge_state) + incorporate_user_merge(merge_state, edit_log_msg=options.edit) except NoManualMergeError, e: raise Failure(str(e)) except NotABlockingCommitError: @@ -2740,8 +3010,7 @@ def main(args): merge_state.simplify(refname, force=options.force) elif options.subcommand == 'finish': require_clean_work_tree('proceed') - options.name = choose_merge_name(options.name, default_to_unique=False) - merge_state = read_merge_state(options.name) + merge_state = read_merge_state(options.name, default_to_unique=False) merge_frontier = MergeFrontier.map_known_frontier(merge_state) if not merge_frontier.is_complete(): raise Failure('Merge %s is not yet complete!' % (merge_state.name,)) @@ -2750,7 +3019,7 @@ def main(args): merge_state.set_goal(options.goal) merge_state.save() merge_state.simplify(refname, force=options.force) - MergeState.remove(options.name) + MergeState.remove(merge_state.name) elif options.subcommand == 'diagram': if not (options.commits or options.frontier): options.frontier = True @@ -2767,7 +3036,7 @@ def main(args): sys.stdout.write('\n') if options.html: merge_frontier = MergeFrontier.map_known_frontier(merge_state) - html = open(options.html, "w") + html = open(options.html, 'w') merge_frontier.write_html(html, merge_state.name) html.close() sys.stdout.write( -- cgit v1.2.3-54-g00ecf