{"thread":{"id":"13442","subject":"[StGit PATCH 2/2] New command: stg undo","startedAt":"2008-05-09T00:44:18Z","lastAt":"2008-05-09T00:44:35Z","messageCount":3,"participants":["Karl Hasselström"],"isPatch":true,"patchVersion":1,"patchTotal":2},"messages":[{"id":"76423","messageId":"20080509003824.20312.17819.stgit@yoghurt","threadId":"13442","inReplyTo":null,"subject":"[StGit PATCH 0/2] stg undo","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-05-09T00:44:18Z","receivedAt":"2008-05-09T00:44:18Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Here's a fully general undo command for StGit. It uses the same\nmachinery as the \"stg reset\" I posted previously, but is more\nconvenient to use when all you want to do is undo the last operation.\n\nThe next step would be to remove all traces of \"push --undo\" et al.,\nbut I'll save that for another day.\n\nFor trying this out, pull from\n\n  git://repo.or.cz/stgit/kha.git experimental\n\nsince a few of the patches below these two have been updated as well.\n(For example, the log branch in <branchname>.stgit^ is now much more\nhuman-readable.)\n\n---\n\nKarl Hasselström (2):\n      New command: stg undo\n      Move stack reset function to a shared location\n\n\n stgit/commands/reset.py  |   70 ++++---------------------------------\n stgit/commands/undo.py   |   49 ++++++++++++++++++++++++++\n stgit/lib/log.py         |   76 +++++++++++++++++++++++++++++++++++++++++\n stgit/lib/transaction.py |    1 +\n stgit/main.py            |    2 +\n t/t3102-undo.sh          |   86 ++++++++++++++++++++++++++++++++++++++++++++++\n t/t3103-undo-hard.sh     |   56 ++++++++++++++++++++++++++++++\n 7 files changed, 277 insertions(+), 63 deletions(-)\n create mode 100644 stgit/commands/undo.py\n create mode 100755 t/t3102-undo.sh\n create mode 100755 t/t3103-undo-hard.sh\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"76424","messageId":"20080509004429.20312.11009.stgit@yoghurt","threadId":"13442","inReplyTo":"20080509003824.20312.17819.stgit@yoghurt","subject":"[StGit PATCH 1/2] Move stack reset function to a shared location","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-05-09T00:44:29Z","receivedAt":"2008-05-09T00:44:29Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Move reset_stack() from commands/reset.py to lib/log.py, so that more\ncommands besides reset can use it. (No such commands exist currently,\nbut undo and redo will use it.)\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/reset.py  |   70 +++++-----------------------------------------\n stgit/lib/log.py         |   57 +++++++++++++++++++++++++++++++++++++\n stgit/lib/transaction.py |    1 +\n 3 files changed, 65 insertions(+), 63 deletions(-)\n\n\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nindex bcc4c6a..2bbac8b 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/reset.py\n@@ -45,67 +45,6 @@ directory = common.DirectoryHasRepositoryLib()\n options = [make_option('--hard', action = 'store_true',\n                        help = 'discard changes in your index/worktree')]\n \n-def reset_stack(stack, iw, state, only_patches, hard):\n-    only_patches = set(only_patches)\n-    def mask(s):\n-        if only_patches:\n-            return s & only_patches\n-        else:\n-            return s\n-    patches_to_reset = mask(set(state.applied + state.unapplied))\n-    existing_patches = set(stack.patchorder.all)\n-    to_delete = mask(existing_patches - patches_to_reset)\n-    trans = transaction.StackTransaction(stack, 'reset', discard_changes = hard)\n-\n-    # If we have to change the stack base, we need to pop all patches\n-    # first.\n-    if not only_patches and trans.base != state.base:\n-        trans.pop_patches(lambda pn: True)\n-        out.info('Setting stack base to %s' % state.base.sha1)\n-        trans.base = state.base\n-\n-    # In one go, do all the popping we have to in order to pop the\n-    # patches we're going to delete or modify.\n-    def mod(pn):\n-        if only_patches and not pn in only_patches:\n-            return False\n-        if pn in to_delete:\n-            return True\n-        if stack.patches.get(pn).commit != state.patches.get(pn, None):\n-            return True\n-        return False\n-    trans.pop_patches(mod)\n-\n-    # Delete and modify/create patches. We've previously popped all\n-    # patches that we touch in this step.\n-    trans.delete_patches(lambda pn: pn in to_delete)\n-    for pn in patches_to_reset:\n-        if pn in existing_patches:\n-            if trans.patches[pn] == state.patches[pn]:\n-                continue\n-            else:\n-                out.info('Resetting %s' % pn)\n-        else:\n-            trans.unapplied.append(pn)\n-            out.info('Resurrecting %s' % pn)\n-        trans.patches[pn] = state.patches[pn]\n-\n-    # Push/pop patches as necessary.\n-    try:\n-        if only_patches:\n-            # Push all the patches that we've popped, if they still\n-            # exist.\n-            pushable = set(trans.unapplied)\n-            for pn in stack.patchorder.applied:\n-                if pn in pushable:\n-                    trans.push_patch(pn, iw)\n-        else:\n-            # Recreate the exact order specified by the goal state.\n-            trans.reorder_patches(state.applied, state.unapplied, iw)\n-    except transaction.TransactionHalted:\n-        pass\n-    return trans.run(iw)\n-\n def func(parser, options, args):\n     stack = directory.repository.current_stack\n     if len(args) >= 1:\n@@ -113,5 +52,10 @@ def func(parser, options, args):\n         state = log.Log(stack.repository, ref, stack.repository.rev_parse(ref))\n     else:\n         raise common.CmdException('Wrong number of arguments')\n-    return reset_stack(stack, stack.repository.default_iw, state, patches,\n-                       options.hard)\n+    trans = transaction.StackTransaction(stack, 'reset',\n+                                         discard_changes = options.hard)\n+    try:\n+        log.reset_stack(trans, stack.repository.default_iw, state, patches)\n+    except transaction.TransactionHalted:\n+        pass\n+    return trans.run(stack.repository.default_iw)\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex e9e89ff..dfc2b9f 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -180,3 +180,60 @@ def copy_log(repo, src_branch, dst_branch, msg):\n \n def default_repo():\n     return stack.Repository.default()\n+\n+def reset_stack(trans, iw, state, only_patches):\n+    only_patches = set(only_patches)\n+    def mask(s):\n+        if only_patches:\n+            return s & only_patches\n+        else:\n+            return s\n+    patches_to_reset = mask(set(state.applied + state.unapplied))\n+    existing_patches = set(trans.all_patches)\n+    original_applied_order = list(trans.applied)\n+    to_delete = mask(existing_patches - patches_to_reset)\n+\n+    # If we have to change the stack base, we need to pop all patches\n+    # first.\n+    if not only_patches and trans.base != state.base:\n+        trans.pop_patches(lambda pn: True)\n+        out.info('Setting stack base to %s' % state.base.sha1)\n+        trans.base = state.base\n+\n+    # In one go, do all the popping we have to in order to pop the\n+    # patches we're going to delete or modify.\n+    def mod(pn):\n+        if only_patches and not pn in only_patches:\n+            return False\n+        if pn in to_delete:\n+            return True\n+        if trans.patches[pn] != state.patches.get(pn, None):\n+            return True\n+        return False\n+    trans.pop_patches(mod)\n+\n+    # Delete and modify/create patches. We've previously popped all\n+    # patches that we touch in this step.\n+    trans.delete_patches(lambda pn: pn in to_delete)\n+    for pn in patches_to_reset:\n+        if pn in existing_patches:\n+            if trans.patches[pn] == state.patches[pn]:\n+                continue\n+            else:\n+                out.info('Resetting %s' % pn)\n+        else:\n+            trans.unapplied.append(pn)\n+            out.info('Resurrecting %s' % pn)\n+        trans.patches[pn] = state.patches[pn]\n+\n+    # Push/pop patches as necessary.\n+    if only_patches:\n+        # Push all the patches that we've popped, if they still\n+        # exist.\n+        pushable = set(trans.unapplied)\n+        for pn in original_applied_order:\n+            if pn in pushable:\n+                trans.push_patch(pn, iw)\n+    else:\n+        # Recreate the exact order specified by the goal state.\n+        trans.reorder_patches(state.applied, state.unapplied, iw)\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex a008780..dd64eb6 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -60,6 +60,7 @@ class StackTransaction(object):\n     def __set_unapplied(self, val):\n         self.__unapplied = list(val)\n     unapplied = property(lambda self: self.__unapplied, __set_unapplied)\n+    all_patches = property(lambda self: self.__applied + self.__unapplied)\n     def __set_base(self, val):\n         assert (not self.__applied\n                 or self.patches[self.applied[0]].data.parent == val)\n"},{"id":"76422","messageId":"20080509004434.20312.14243.stgit@yoghurt","threadId":"13442","inReplyTo":"20080509003824.20312.17819.stgit@yoghurt","subject":"[StGit PATCH 2/2] New command: stg undo","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-05-09T00:44:35Z","receivedAt":"2008-05-09T00:44:35Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Basically, this is just a user-friendly way to access a subset of the\nfunctionality of \"stg reset\".\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/undo.py  |   38 +++++++--------------\n stgit/lib/log.py        |   19 ++++++++++\n stgit/main.py           |    2 +\n t/t3102-undo.sh         |   86 +++++++++++++++++++++++++++++++++++++++++++++++\n t/t3103-undo-hard.sh    |   10 +++--\n 5 files changed, 125 insertions(+), 30 deletions(-)\n copy stgit/commands/{reset.py => undo.py} (58%)\n create mode 100755 t/t3102-undo.sh\n copy t/{t3101-reset-hard.sh => t3103-undo-hard.sh} (82%)\n\n\ndiff --git a/stgit/commands/reset.py b/stgit/commands/undo.py\nsimilarity index 58%\ncopy from stgit/commands/reset.py\ncopy to stgit/commands/undo.py\nindex 2bbac8b..b1d7de9 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/undo.py\n@@ -22,40 +22,28 @@ from stgit.commands import common\n from stgit.lib import git, log, transaction\n from stgit.out import out\n \n-help = 'reset the patch stack to an earlier state'\n-usage = \"\"\"%prog [options] <state> [<patchnames>]\n+help = 'undo the last operation'\n+usage = \"\"\"%prog [options]\n \n-Reset the patch stack to an earlier state. The state is specified with\n-a commit from a stack log; for a branch foo, StGit stores the stack\n-log in foo.stgit^. So to undo the last N StGit commands, you would say\n-\n-  stg reset foo.stgit^~N\n-\n-or, if you are not sure how many steps to undo, you can view the log\n-with \"git log\" or gitk\n-\n-  gitk foo.stgit^\n-\n-and then reset to any sha1 you see in the log.\n-\n-If one or more patch names are given, reset only those patches, and\n-leave the rest alone.\"\"\"\n+Reset the patch stack to the previous state. Consecutive invocations\n+of \"stg undo\" will take you ever further into the past.\"\"\"\n \n directory = common.DirectoryHasRepositoryLib()\n-options = [make_option('--hard', action = 'store_true',\n+options = [make_option('-n', '--number', type = 'int', metavar = 'N',\n+                       default = 1,\n+                       help = 'undo the last N commands'),\n+           make_option('--hard', action = 'store_true',\n                        help = 'discard changes in your index/worktree')]\n \n def func(parser, options, args):\n     stack = directory.repository.current_stack\n-    if len(args) >= 1:\n-        ref, patches = args[0], args[1:]\n-        state = log.Log(stack.repository, ref, stack.repository.rev_parse(ref))\n-    else:\n-        raise common.CmdException('Wrong number of arguments')\n-    trans = transaction.StackTransaction(stack, 'reset',\n+    if options.number < 1:\n+        raise common.CmdException('Bad number of commands to undo')\n+    state = log.undo_state(stack, options.number)\n+    trans = transaction.StackTransaction(stack, 'undo %d' % options.number,\n                                          discard_changes = options.hard)\n     try:\n-        log.reset_stack(trans, stack.repository.default_iw, state, patches)\n+        log.reset_stack(trans, stack.repository.default_iw, state, [])\n     except transaction.TransactionHalted:\n         pass\n     return trans.run(stack.repository.default_iw)\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex dfc2b9f..6d9fc6e 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -1,3 +1,4 @@\n+import re\n from stgit.lib import git, stack\n from stgit import exception\n from stgit.out import out\n@@ -155,6 +156,9 @@ class Log(object):\n             self.base = self.patches[self.applied[0]].data.parent\n         else:\n             self.base = self.head\n+    @property\n+    def parents(self):\n+        return self.commit.data.parents[2:]\n \n class FullLog(Log):\n     full_log = property(lambda self: self.commit)\n@@ -237,3 +241,18 @@ def reset_stack(trans, iw, state, only_patches):\n     else:\n         # Recreate the exact order specified by the goal state.\n         trans.reorder_patches(state.applied, state.unapplied, iw)\n+\n+def undo_state(stack, undo_steps):\n+    ref = log_ref(stack.name)\n+    log = Log(stack.repository, ref, stack.repository.refs.get(ref))\n+    while undo_steps > 0:\n+        msg = log.commit.data.message.strip()\n+        m = re.match(r'^undo\\s+(\\d+)$', msg)\n+        if m:\n+            undo_steps += int(m.group(1))\n+        else:\n+            undo_steps -= 1\n+        if not log.parents:\n+            raise LogException('Not enough undo information available')\n+        log = Log(stack.repository, log.parents[0].sha1, log.parents[0])\n+    return log\ndiff --git a/stgit/main.py b/stgit/main.py\nindex 83e6b08..cf7b404 100644\n--- a/stgit/main.py\n+++ b/stgit/main.py\n@@ -99,6 +99,7 @@ commands = Commands({\n     'top':              'top',\n     'unapplied':        'unapplied',\n     'uncommit':         'uncommit',\n+    'undo':             'undo',\n     'unhide':           'unhide'\n     })\n \n@@ -129,6 +130,7 @@ stackcommands = (\n     'top',\n     'unapplied',\n     'uncommit',\n+    'undo',\n     'unhide',\n     )\n patchcommands = (\ndiff --git a/t/t3102-undo.sh b/t/t3102-undo.sh\nnew file mode 100755\nindex 0000000..1093f70\n--- /dev/null\n+++ b/t/t3102-undo.sh\n@@ -0,0 +1,86 @@\n+#!/bin/sh\n+\n+test_description='Simple test cases for \"stg undo\"'\n+\n+. ./test-lib.sh\n+\n+# Ignore our own output files.\n+cat > .git/info/exclude <<EOF\n+/expected.txt\n+EOF\n+\n+test_expect_success 'Initialize StGit stack with three patches' '\n+    stg init &&\n+    echo 000 >> a &&\n+    git add a &&\n+    git commit -m a &&\n+    echo 111 >> a &&\n+    git commit -a -m p1 &&\n+    echo 222 >> a &&\n+    git commit -a -m p2 &&\n+    echo 333 >> a &&\n+    git commit -a -m p3 &&\n+    stg uncommit -n 3 &&\n+    stg pop\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+EOF\n+test_expect_success 'Pop one patch ...' '\n+    stg pop &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p2 p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+222\n+EOF\n+test_expect_success '... and undo it' '\n+    stg undo &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+EOF\n+test_expect_success 'Pop two patches ...' '\n+    stg pop &&\n+    stg pop &&\n+    test \"$(echo $(stg applied))\" = \"\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p1 p2 p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+222\n+EOF\n+test_expect_success '... and undo it' '\n+    stg undo &&\n+    stg undo &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+222\n+EOF\n+test_expect_success 'Undo past end of history' '\n+    ! stg undo -n 100 &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_done\ndiff --git a/t/t3101-reset-hard.sh b/t/t3103-undo-hard.sh\nsimilarity index 82%\ncopy from t/t3101-reset-hard.sh\ncopy to t/t3103-undo-hard.sh\nindex 1e02805..21412f7 100755\n--- a/t/t3101-reset-hard.sh\n+++ b/t/t3103-undo-hard.sh\n@@ -1,6 +1,6 @@\n #!/bin/sh\n \n-test_description='Simple test cases for \"stg reset\"'\n+test_description='Simple test cases for \"stg undo\"'\n \n . ./test-lib.sh\n \n@@ -35,8 +35,8 @@ test_expect_success 'Pop middle patch, creating a conflict' '\n     test \"$(echo $(stg unapplied))\" = \"p2\"\n '\n \n-test_expect_success 'Try to reset without --hard' '\n-    ! stg reset master.stgit^~1 &&\n+test_expect_success 'Try to undo without --hard' '\n+    ! stg undo &&\n     stg status a > actual.txt &&\n     test_cmp expected.txt actual.txt &&\n     test \"$(echo $(stg applied))\" = \"p1 p3\" &&\n@@ -45,8 +45,8 @@ test_expect_success 'Try to reset without --hard' '\n \n cat > expected.txt <<EOF\n EOF\n-test_expect_success 'Try to reset with --hard' '\n-    stg reset --hard master.stgit^~1 &&\n+test_expect_success 'Try to undo with --hard' '\n+    stg undo --hard &&\n     stg status a > actual.txt &&\n     test_cmp expected.txt actual.txt &&\n     test \"$(echo $(stg applied))\" = \"p1\" &&\n"}]}