from stgit.utils import any, all
from stgit.out import *
from stgit.lib import git, log
+from stgit.config import config
class TransactionException(exception.StgException):
"""Exception raised when something goes wrong with a
your refs and index+worktree, or fail without having done
anything."""
def __init__(self, stack, msg, discard_changes = False,
- allow_conflicts = False, allow_bad_head = False):
+ allow_conflicts = False, allow_bad_head = False,
+ check_clean_iw = None):
"""Create a new L{StackTransaction}.
@param discard_changes: Discard any changes in index+worktree
self.__base = self.__stack.base
self.__discard_changes = discard_changes
self.__bad_head = None
+ self.__conflicts = None
if isinstance(allow_conflicts, bool):
self.__allow_conflicts = lambda trans: allow_conflicts
else:
self.__temp_index = self.temp_index_tree = None
if not allow_bad_head:
self.__assert_head_top_equal()
+ if check_clean_iw:
+ self.__assert_index_worktree_clean(check_clean_iw)
stack = property(lambda self: self.__stack)
patches = property(lambda self: self.__patches)
def __set_applied(self, val):
'This can happen if you modify a branch with git.',
'"stg repair --help" explains more about what to do next.')
self.__abort()
+ def __assert_index_worktree_clean(self, iw):
+ if not iw.worktree_clean():
+ self.__halt('Worktree not clean. Use "refresh" or "status --reset"')
+ if not iw.index.is_clean(self.stack.head):
+ self.__halt('Index not clean. Use "refresh" or "status --reset"')
def __checkout(self, tree, iw, allow_bad_head):
if not allow_bad_head:
self.__assert_head_top_equal()
self.__stack.set_head(new_head, self.__msg)
if self.__error:
- out.error(self.__error)
+ if self.__conflicts:
+ out.error(*([self.__error] + self.__conflicts))
+ else:
+ out.error(self.__error)
# Write patches.
def write(msg):
out.info('Deleted %s%s' % (pn, s))
return popped
- def push_patch(self, pn, iw = None):
+ def push_patch(self, pn, iw = None, allow_interactive = False,
+ already_merged = False):
"""Attempt to push the named patch. If this results in conflicts,
halts the transaction. If index+worktree are given, spill any
conflicts to them."""
+ out.start('Pushing patch "%s"' % pn)
orig_cd = self.patches[pn].data
cd = orig_cd.set_committer(None)
- s = ['', ' (empty)'][cd.is_nochange()]
oldparent = cd.parent
cd = cd.set_parent(self.top)
- base = oldparent.data.tree
- ours = cd.parent.data.tree
- theirs = cd.tree
- tree, self.temp_index_tree = self.temp_index.merge(
- base, ours, theirs, self.temp_index_tree)
+ if already_merged:
+ # the resulting patch is empty
+ tree = cd.parent.data.tree
+ else:
+ base = oldparent.data.tree
+ ours = cd.parent.data.tree
+ theirs = cd.tree
+ tree, self.temp_index_tree = self.temp_index.merge(
+ base, ours, theirs, self.temp_index_tree)
+ s = ''
merge_conflict = False
if not tree:
if iw == None:
except git.CheckoutException:
self.__halt('Index/worktree dirty')
try:
- iw.merge(base, ours, theirs)
+ interactive = (allow_interactive and
+ config.get('stgit.autoimerge') == 'yes')
+ iw.merge(base, ours, theirs, interactive = interactive)
tree = iw.index.write_tree()
self.__current_tree = tree
- s = ' (modified)'
- except git.MergeConflictException:
+ s = 'modified'
+ except git.MergeConflictException, e:
tree = ours
merge_conflict = True
- s = ' (conflict)'
+ self.__conflicts = e.conflicts
+ s = 'conflict'
except git.MergeException, e:
self.__halt(str(e))
cd = cd.set_tree(tree)
if any(getattr(cd, a) != getattr(orig_cd, a) for a in
['parent', 'tree', 'author', 'message']):
comm = self.__stack.repository.commit(cd)
+ if merge_conflict:
+ # When we produce a conflict, we'll run the update()
+ # function defined below _after_ having done the
+ # checkout in run(). To make sure that we check out
+ # the real stack top (as it will look after update()
+ # has been run), set it hard here.
+ self.head = comm
else:
comm = None
- s = ' (unmodified)'
- out.info('Pushed %s%s' % (pn, s))
+ s = 'unmodified'
+ if already_merged:
+ s = 'merged'
+ elif not merge_conflict and cd.is_nochange():
+ s = 'empty'
+ out.done(s)
def update():
if comm:
self.patches[pn] = comm
# Save this update so that we can run it a little later.
self.__conflicting_push = update
- self.__halt('Merge conflict')
+ self.__halt("%d merge conflict(s)" % len(self.__conflicts))
else:
# Update immediately.
update()
- def reorder_patches(self, applied, unapplied, hidden, iw = None):
+ def push_tree(self, pn):
+ """Push the named patch without updating its tree."""
+ orig_cd = self.patches[pn].data
+ cd = orig_cd.set_committer(None).set_parent(self.top)
+
+ s = ''
+ if any(getattr(cd, a) != getattr(orig_cd, a) for a in
+ ['parent', 'tree', 'author', 'message']):
+ self.patches[pn] = self.__stack.repository.commit(cd)
+ else:
+ s = ' (unmodified)'
+ if cd.is_nochange():
+ s = ' (empty)'
+ out.info('Pushed %s%s' % (pn, s))
+
+ if pn in self.hidden:
+ x = self.hidden
+ else:
+ x = self.unapplied
+ del x[x.index(pn)]
+ self.applied.append(pn)
+
+ def reorder_patches(self, applied, unapplied, hidden = None, iw = None):
"""Push and pop patches to attain the given ordering."""
+ if hidden is None:
+ hidden = self.hidden
common = len(list(it.takewhile(lambda (a, b): a == b,
zip(self.applied, applied))))
to_pop = set(self.applied[common:])
assert set(self.unapplied + self.hidden) == set(unapplied + hidden)
self.unapplied = unapplied
self.hidden = hidden
+
+ def check_merged(self, patches):
+ """Return a subset of patches already merged."""
+ out.start('Checking for patches merged upstream')
+ merged = []
+ if self.temp_index_tree != self.stack.head.data.tree:
+ self.temp_index.read_tree(self.stack.head.data.tree)
+ self.temp_index_tree = self.stack.head.data.tree
+ for pn in reversed(patches):
+ # check whether patch changes can be reversed in the current index
+ cd = self.patches[pn].data
+ if cd.is_nochange():
+ continue
+ try:
+ self.temp_index.apply_treediff(cd.tree, cd.parent.data.tree,
+ quiet = True)
+ merged.append(pn)
+ # The self.temp_index was modified by apply_treediff() so
+ # force read_tree() the next time merge() is used.
+ self.temp_index_tree = None
+ except git.MergeException:
+ pass
+ out.done('%d found' % len(merged))
+ return merged