chiark / gitweb /
New command: stg redo
[stgit] / stgit / lib / log.py
index 9568e8f9d1b77d27bb3c222467fc062e59355afc..448532dd806ccd7f0d92a2adf963cfed3f8bc0ed 100644 (file)
@@ -99,6 +99,7 @@ The simplified log is exactly like the full log, except that its only
 parent is the (simplified) previous log entry, if any. It's purpose is
 mainly ease of visualization."""
 
+import re
 from stgit.lib import git, stack as libstack
 from stgit import exception, utils
 from stgit.out import out
@@ -157,6 +158,12 @@ class LogEntry(object):
             return self.patches[self.applied[0]].data.parent
         else:
             return self.head
+    @property
+    def top(self):
+        if self.applied:
+            return self.patches[self.applied[-1]]
+        else:
+            return self.head
     @classmethod
     def from_stack(cls, prev, stack, message):
         return cls(
@@ -382,3 +389,106 @@ def copy_log(repo, src_branch, dst_branch, msg):
 
 def default_repo():
     return libstack.Repository.default()
+
+def reset_stack(trans, iw, state, only_patches):
+    """Reset the stack to a given previous state. If C{only_patches} is
+    not empty, touch only patches whose names appear in it.
+
+    @param only_patches: Reset only these patches
+    @type only_patches: iterable"""
+    only_patches = set(only_patches)
+    def mask(s):
+        if only_patches:
+            return s & only_patches
+        else:
+            return s
+    patches_to_reset = mask(set(state.applied + state.unapplied + state.hidden))
+    existing_patches = set(trans.all_patches)
+    original_applied_order = list(trans.applied)
+    to_delete = mask(existing_patches - patches_to_reset)
+
+    # If we have to change the stack base, we need to pop all patches
+    # first.
+    if not only_patches and trans.base != state.base:
+        trans.pop_patches(lambda pn: True)
+        out.info('Setting stack base to %s' % state.base.sha1)
+        trans.base = state.base
+
+    # In one go, do all the popping we have to in order to pop the
+    # patches we're going to delete or modify.
+    def mod(pn):
+        if only_patches and not pn in only_patches:
+            return False
+        if pn in to_delete:
+            return True
+        if trans.patches[pn] != state.patches.get(pn, None):
+            return True
+        return False
+    trans.pop_patches(mod)
+
+    # Delete and modify/create patches. We've previously popped all
+    # patches that we touch in this step.
+    trans.delete_patches(lambda pn: pn in to_delete)
+    for pn in patches_to_reset:
+        if pn in existing_patches:
+            if trans.patches[pn] == state.patches[pn]:
+                continue
+            else:
+                out.info('Resetting %s' % pn)
+        else:
+            if pn in state.hidden:
+                trans.hidden.append(pn)
+            else:
+                trans.unapplied.append(pn)
+            out.info('Resurrecting %s' % pn)
+        trans.patches[pn] = state.patches[pn]
+
+    # Push/pop patches as necessary.
+    if only_patches:
+        # Push all the patches that we've popped, if they still
+        # exist.
+        pushable = set(trans.unapplied)
+        for pn in original_applied_order:
+            if pn in pushable:
+                trans.push_patch(pn, iw)
+    else:
+        # Recreate the exact order specified by the goal state.
+        trans.reorder_patches(state.applied, state.unapplied, state.hidden, iw)
+
+def undo_state(stack, undo_steps):
+    """Find the log entry C{undo_steps} steps in the past. (Successive
+    undo operations are supposed to "add up", so if we find other undo
+    operations along the way we have to add those undo steps to
+    C{undo_steps}.)
+
+    If C{undo_steps} is negative, redo instead of undo.
+
+    @return: The log entry that is the destination of the undo
+             operation
+    @rtype: L{LogEntry}"""
+    ref = log_ref(stack.name)
+    try:
+        commit = stack.repository.refs.get(ref)
+    except KeyError:
+        raise LogException('Log is empty')
+    log = get_log_entry(stack.repository, ref, commit)
+    while undo_steps != 0:
+        msg = log.message.strip()
+        um = re.match(r'^undo\s+(\d+)$', msg)
+        if undo_steps > 0:
+            if um:
+                undo_steps += int(um.group(1))
+            else:
+                undo_steps -= 1
+        else:
+            rm = re.match(r'^redo\s+(\d+)$', msg)
+            if um:
+                undo_steps += 1
+            elif rm:
+                undo_steps -= int(rm.group(1))
+            else:
+                raise LogException('No more redo information available')
+        if not log.prev:
+            raise LogException('Not enough undo information available')
+        log = log.prev
+    return log