summaryrefslogtreecommitdiffstats
path: root/bin/git
diff options
context:
space:
mode:
authorJesse Luehrs <doy@tozt.net>2013-10-27 23:54:08 -0400
committerJesse Luehrs <doy@tozt.net>2013-10-27 23:57:33 -0400
commit647e000e0d5021d7684ade725599ad78634f8ad4 (patch)
tree7d24ca7137e517e501a590e055bc1e51cd2ad663 /bin/git
parentdad82f6d1eaf565300fc6833f6da43b2f1097801 (diff)
downloadconf-647e000e0d5021d7684ade725599ad78634f8ad4.tar.gz
conf-647e000e0d5021d7684ade725599ad78634f8ad4.zip
update some things from their external sources
Diffstat (limited to 'bin/git')
-rwxr-xr-xbin/git/git-imerge729
1 files changed, 499 insertions, 230 deletions
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:
<fix conflict presented to you>
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(" <tr>\n")
+ f.write(' <tr>\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(" <td%s%s>%.*s</td>\n" % (
+ td_class = classes and ' class="' + ' '.join(classes) + '"' or ''
+ f.write(' <td%s%s>%.*s</td>\n' % (
td_id, td_class, abbrev_sha1, sha1))
- f.write(" </tr>\n")
- f.write("</table>\n</body>\n</html>\n")
+ f.write(' </tr>\n')
+ f.write('</table>\n</body>\n</html>\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(