Add a --set-tree flag to stg push
[stgit] / stgit / lib / transaction.py
index 6623645..30a153b 100644 (file)
@@ -8,6 +8,7 @@ from stgit import exception, utils
 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
@@ -75,7 +76,8 @@ class StackTransaction(object):
       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
@@ -94,6 +96,7 @@ class StackTransaction(object):
         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:
@@ -101,6 +104,8 @@ class StackTransaction(object):
         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):
@@ -146,6 +151,11 @@ class StackTransaction(object):
                 '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()
@@ -201,7 +211,10 @@ class StackTransaction(object):
             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):
@@ -284,19 +297,25 @@ class StackTransaction(object):
                     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)
         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:
@@ -307,26 +326,38 @@ class StackTransaction(object):
             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)'
-        if not merge_conflict and cd.is_nochange():
-            s = ' (empty)'
-        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
@@ -343,13 +374,37 @@ class StackTransaction(object):
 
             # 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:])
@@ -360,3 +415,27 @@ class StackTransaction(object):
         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