{"thread":{"id":"13191","subject":"[StGit PATCH 00/10] Updated \"stg reset\" series","startedAt":"2008-04-20T22:10:20Z","lastAt":"2008-04-20T22:11:18Z","messageCount":11,"participants":["Karl Hasselström"],"isPatch":true,"patchVersion":1,"patchTotal":10},"messages":[{"id":"74836","messageId":"20080420215625.5837.82896.stgit@yoghurt","threadId":"13191","inReplyTo":null,"subject":"[StGit PATCH 00/10] Updated \"stg reset\" series","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:20Z","receivedAt":"2008-04-20T22:10:20Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"The patch stack log / stg reset series has matured over the weekend.\nThe major highlights are\n\n  1. When a command fails to push a patch due to conflicts, it will\n     log two entries -- one for the part of the command that\n     succeeded, and one just for the conflicting push. This makes it\n     possible to undo just the last part, and not the whole command.\n\n  2. I've tightened the checks so that conflicts will prevent most\n     commands from working unless they actively claim to make sense in\n     the face of conflicts. Basically, only commands that don't touch\n     the topmost patch should be able to run when there are conflicts.\n\n  3. stg reset now understands --hard, which makes it zonk any local\n     changes, including conflicts.\n\n(1) and (3) means that\n\n  $ stg reset --hard <branchname>.stgit^~1\n\ncan be used instead of stg push --undo when you want to undo a\nconflicting push. Now I just have to make that available as\n\n  $ stg undo\n\nso that it actually becomes usable ...\n\nThese patches are available at\n\n  git://repo.or.cz/stgit/kha.git experimental\n\n---\n\nKarl Hasselström (10):\n      Don't write a log entry if there were no changes\n      Add a --hard flag to stg reset\n      Log conflicts separately for all commands\n      Log conflicts separately\n      New command: stg reset\n      Add utility function for reordering patches\n      Write to a stack log when stack is modified\n      Library functions for tree and blob manipulation\n      Add property with a list of all patch names\n      Prevent most commands from running when there are conflicts\n\n\n stgit/commands/branch.py   |   20 ++++-\n stgit/commands/clean.py    |    3 +\n stgit/commands/coalesce.py |    6 +-\n stgit/commands/commit.py   |   13 ++--\n stgit/commands/common.py   |    8 ++\n stgit/commands/delete.py   |   14 +++-\n stgit/commands/diff.py     |    2 -\n stgit/commands/edit.py     |    3 +\n stgit/commands/export.py   |    2 -\n stgit/commands/files.py    |    2 -\n stgit/commands/id.py       |    2 -\n stgit/commands/log.py      |    2 -\n stgit/commands/mail.py     |    2 -\n stgit/commands/patches.py  |    2 -\n stgit/commands/reset.py    |  118 ++++++++++++++++++++++++++++++++\n stgit/commands/series.py   |    2 -\n stgit/commands/show.py     |    2 -\n stgit/commands/status.py   |    3 +\n stgit/commands/top.py      |    2 -\n stgit/commands/uncommit.py |    5 +\n stgit/lib/git.py           |  143 +++++++++++++++++++++++++++++++++------\n stgit/lib/log.py           |  161 ++++++++++++++++++++++++++++++++++++++++++++\n stgit/lib/stack.py         |    8 ++\n stgit/lib/transaction.py   |   92 ++++++++++++++++++++-----\n stgit/main.py              |    4 +\n t/t3100-reset.sh           |  151 +++++++++++++++++++++++++++++++++++++++++\n t/t3101-reset-hard.sh      |   56 +++++++++++++++\n 27 files changed, 754 insertions(+), 74 deletions(-)\n create mode 100644 stgit/commands/reset.py\n create mode 100644 stgit/lib/log.py\n create mode 100755 t/t3100-reset.sh\n create mode 100755 t/t3101-reset-hard.sh\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"74839","messageId":"20080420221027.5837.19012.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 01/10] Prevent most commands from running when there are conflicts","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:27Z","receivedAt":"2008-04-20T22:10:27Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"When there are conflicts, we want most commands to fail, since the\nconflicts conceptually belong to the topmost patch. git read-tree\nalready checks this for us when we check out a new tree, but there are\noperations where the top tree stays the same, e.g. stg new.\n\nThis patch inserts a conflict check when the tree to check out is the\nsame. By default, conflicts will prevent the checkout from succeeding,\nbut commands can choose to override this if the same patch stays on\ntop (for some definition of \"same\").\n\nThis change only affects the new-infrastructure commands; the others\nalways refuse to run when there are local changes of any kind.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/clean.py    |    3 ++-\n stgit/commands/coalesce.py |    3 ++-\n stgit/commands/commit.py   |    7 ++++++-\n stgit/commands/delete.py   |   10 +++++++++-\n stgit/commands/edit.py     |    3 ++-\n stgit/commands/uncommit.py |    3 ++-\n stgit/lib/transaction.py   |   27 ++++++++++++++++++++++-----\n 7 files changed, 45 insertions(+), 11 deletions(-)\n\n\ndiff --git a/stgit/commands/clean.py b/stgit/commands/clean.py\nindex 889c1dc..ed6fc6e 100644\n--- a/stgit/commands/clean.py\n+++ b/stgit/commands/clean.py\n@@ -37,7 +37,8 @@ options = [make_option('-a', '--applied',\n \n \n def _clean(stack, clean_applied, clean_unapplied):\n-    trans = transaction.StackTransaction(stack, 'stg clean')\n+    trans = transaction.StackTransaction(stack, 'stg clean',\n+                                         allow_conflicts = True)\n     def del_patch(pn):\n         if pn in stack.patchorder.applied:\n             if pn == stack.patchorder.applied[-1]:\ndiff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py\nindex 291a537..bf40427 100644\n--- a/stgit/commands/coalesce.py\n+++ b/stgit/commands/coalesce.py\n@@ -79,7 +79,8 @@ def _coalesce(stack, iw, name, msg, save_template, patches):\n         trans.patches[name] = stack.repository.commit(new_commit_data)\n         trans.unapplied.insert(0, name)\n \n-    trans = transaction.StackTransaction(stack, 'stg coalesce')\n+    trans = transaction.StackTransaction(stack, 'stg coalesce',\n+                                         allow_conflicts = True)\n     push_new_patch = bool(set(patches) & set(trans.applied))\n     try:\n         new_commit_data = _coalesce_patches(trans, patches, msg, save_template)\ndiff --git a/stgit/commands/commit.py b/stgit/commands/commit.py\nindex bff94ce..ee95836 100644\n--- a/stgit/commands/commit.py\n+++ b/stgit/commands/commit.py\n@@ -69,7 +69,12 @@ def func(parser, options, args):\n         raise common.CmdException('No patches to commit')\n \n     iw = stack.repository.default_iw\n-    trans = transaction.StackTransaction(stack, 'stg commit')\n+    def allow_conflicts(trans):\n+        # As long as the topmost patch stays where it is, it's OK to\n+        # run \"stg commit\" with conflicts in the index.\n+        return len(trans.applied) >= 1\n+    trans = transaction.StackTransaction(stack, 'stg commit',\n+                                         allow_conflicts = allow_conflicts)\n     try:\n         common_prefix = 0\n         for i in xrange(min(len(stack.patchorder.applied), len(patches))):\ndiff --git a/stgit/commands/delete.py b/stgit/commands/delete.py\nindex 106fbd2..14bf442 100644\n--- a/stgit/commands/delete.py\n+++ b/stgit/commands/delete.py\n@@ -46,7 +46,15 @@ def func(parser, options, args):\n                        + list(stack.patchorder.unapplied))))\n     else:\n         parser.error('No patches specified')\n-    trans = transaction.StackTransaction(stack, 'stg delete')\n+    def allow_conflicts(trans):\n+        # Allow conflicts if the topmost patch stays the same.\n+        if stack.patchorder.applied:\n+            return (trans.applied\n+                    and trans.applied[-1] == stack.patchorder.applied[-1])\n+        else:\n+            return not trans.applied\n+    trans = transaction.StackTransaction(stack, 'stg delete',\n+                                         allow_conflicts = allow_conflicts)\n     try:\n         to_push = trans.delete_patches(lambda pn: pn in patches)\n         for pn in to_push:\ndiff --git a/stgit/commands/edit.py b/stgit/commands/edit.py\nindex eff9117..c354730 100644\n--- a/stgit/commands/edit.py\n+++ b/stgit/commands/edit.py\n@@ -173,7 +173,8 @@ def func(parser, options, args):\n     # The patch applied, so now we have to rewrite the StGit patch\n     # (and any patches on top of it).\n     iw = stack.repository.default_iw\n-    trans = transaction.StackTransaction(stack, 'stg edit')\n+    trans = transaction.StackTransaction(stack, 'stg edit',\n+                                         allow_conflicts = True)\n     if patchname in trans.applied:\n         popped = trans.applied[trans.applied.index(patchname)+1:]\n         assert not trans.pop_patches(lambda pn: pn in popped)\ndiff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py\nindex 272c5db..b6765bc 100644\n--- a/stgit/commands/uncommit.py\n+++ b/stgit/commands/uncommit.py\n@@ -131,7 +131,8 @@ def func(parser, options, args):\n             taken_names.add(pn)\n         patchnames.reverse()\n \n-    trans = transaction.StackTransaction(stack, 'stg uncommit')\n+    trans = transaction.StackTransaction(stack, 'stg uncommit',\n+                                         allow_conflicts = True)\n     for commit, pn in zip(commits, patchnames):\n         trans.patches[pn] = commit\n     trans.applied = list(reversed(patchnames)) + trans.applied\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 1ece01e..874f81b 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -34,7 +34,7 @@ class _TransPatchMap(dict):\n             return self.__stack.patches.get(pn).commit\n \n class StackTransaction(object):\n-    def __init__(self, stack, msg):\n+    def __init__(self, stack, msg, allow_conflicts = False):\n         self.__stack = stack\n         self.__msg = msg\n         self.__patches = _TransPatchMap(stack)\n@@ -43,6 +43,10 @@ class StackTransaction(object):\n         self.__error = None\n         self.__current_tree = self.__stack.head.data.tree\n         self.__base = self.__stack.base\n+        if isinstance(allow_conflicts, bool):\n+            self.__allow_conflicts = lambda trans: allow_conflicts\n+        else:\n+            self.__allow_conflicts = allow_conflicts\n     stack = property(lambda self: self.__stack)\n     patches = property(lambda self: self.__patches)\n     def __set_applied(self, val):\n@@ -63,10 +67,19 @@ class StackTransaction(object):\n                 'This can happen if you modify a branch with git.',\n                 '\"stg repair --help\" explains more about what to do next.')\n             self.__abort()\n-        if self.__current_tree != tree:\n-            assert iw != None\n-            iw.checkout(self.__current_tree, tree)\n-            self.__current_tree = tree\n+        if self.__current_tree == tree:\n+            # No tree change, but we still want to make sure that\n+            # there are no unresolved conflicts. Conflicts\n+            # conceptually \"belong\" to the topmost patch, and just\n+            # carrying them along to another patch is confusing.\n+            if (self.__allow_conflicts(self) or iw == None\n+                or not iw.index.conflicts()):\n+                return\n+            out.error('Need to resolve conflicts first')\n+            self.__abort()\n+        assert iw != None\n+        iw.checkout(self.__current_tree, tree)\n+        self.__current_tree = tree\n     @staticmethod\n     def __abort():\n         raise TransactionException(\n@@ -214,4 +227,8 @@ class StackTransaction(object):\n         self.applied.append(pn)\n         out.info('Pushed %s%s' % (pn, s))\n         if merge_conflict:\n+            # We've just caused conflicts, so we must allow them in\n+            # the final checkout.\n+            self.__allow_conflicts = lambda trans: True\n+\n             self.__halt('Merge conflict')\n"},{"id":"74837","messageId":"20080420221033.5837.39163.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 02/10] Add property with a list of all patch names","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:33Z","receivedAt":"2008-04-20T22:10:33Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"This simplifies the code in a number of places.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/coalesce.py |    3 +--\n stgit/commands/commit.py   |    6 ++----\n stgit/commands/delete.py   |    4 +---\n stgit/commands/uncommit.py |    2 +-\n stgit/lib/stack.py         |    1 +\n 5 files changed, 6 insertions(+), 10 deletions(-)\n\n\ndiff --git a/stgit/commands/coalesce.py b/stgit/commands/coalesce.py\nindex bf40427..9a46097 100644\n--- a/stgit/commands/coalesce.py\n+++ b/stgit/commands/coalesce.py\n@@ -115,8 +115,7 @@ def _coalesce(stack, iw, name, msg, save_template, patches):\n \n def func(parser, options, args):\n     stack = directory.repository.current_stack\n-    patches = common.parse_patches(args, (list(stack.patchorder.applied)\n-                                          + list(stack.patchorder.unapplied)))\n+    patches = common.parse_patches(args, list(stack.patchorder.all))\n     if len(patches) < 2:\n         raise common.CmdException('Need at least two patches')\n     return _coalesce(stack, stack.repository.default_iw, options.name,\ndiff --git a/stgit/commands/commit.py b/stgit/commands/commit.py\nindex ee95836..ab89590 100644\n--- a/stgit/commands/commit.py\n+++ b/stgit/commands/commit.py\n@@ -45,13 +45,11 @@ options = [make_option('-n', '--number', type = 'int',\n def func(parser, options, args):\n     \"\"\"Commit a number of patches.\"\"\"\n     stack = directory.repository.current_stack\n-    args = common.parse_patches(args, (list(stack.patchorder.applied)\n-                                       + list(stack.patchorder.unapplied)))\n+    args = common.parse_patches(args, list(stack.patchorder.all))\n     if len([x for x in [args, options.number != None, options.all] if x]) > 1:\n         parser.error('too many options')\n     if args:\n-        patches = [pn for pn in (stack.patchorder.applied\n-                                 + stack.patchorder.unapplied) if pn in args]\n+        patches = [pn for pn in stack.patchorder.all if pn in args]\n         bad = set(args) - set(patches)\n         if bad:\n             raise common.CmdException('Bad patch names: %s'\ndiff --git a/stgit/commands/delete.py b/stgit/commands/delete.py\nindex 14bf442..872ed77 100644\n--- a/stgit/commands/delete.py\n+++ b/stgit/commands/delete.py\n@@ -41,9 +41,7 @@ def func(parser, options, args):\n         stack = directory.repository.current_stack\n         iw = stack.repository.default_iw\n     if args:\n-        patches = set(common.parse_patches(\n-                args, (list(stack.patchorder.applied)\n-                       + list(stack.patchorder.unapplied))))\n+        patches = set(common.parse_patches(args, list(stack.patchorder.all)))\n     else:\n         parser.error('No patches specified')\n     def allow_conflicts(trans):\ndiff --git a/stgit/commands/uncommit.py b/stgit/commands/uncommit.py\nindex b6765bc..9ab178c 100644\n--- a/stgit/commands/uncommit.py\n+++ b/stgit/commands/uncommit.py\n@@ -116,7 +116,7 @@ def func(parser, options, args):\n             next_commit = get_parent(next_commit)\n         patch_nr = len(commits)\n \n-    taken_names = set(stack.patchorder.applied + stack.patchorder.unapplied)\n+    taken_names = set(stack.patchorder.all)\n     if patchnames:\n         for pn in patchnames:\n             if pn in taken_names:\ndiff --git a/stgit/lib/stack.py b/stgit/lib/stack.py\nindex 3de3776..af1c994 100644\n--- a/stgit/lib/stack.py\n+++ b/stgit/lib/stack.py\n@@ -96,6 +96,7 @@ class PatchOrder(object):\n                        lambda self, val: self.__set_list('applied', val))\n     unapplied = property(lambda self: self.__get_list('unapplied'),\n                          lambda self, val: self.__set_list('unapplied', val))\n+    all = property(lambda self: self.applied + self.unapplied)\n \n class Patches(object):\n     \"\"\"Creates Patch objects.\"\"\"\n"},{"id":"74838","messageId":"20080420221038.5837.75890.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 03/10] Library functions for tree and blob manipulation","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:38Z","receivedAt":"2008-04-20T22:10:38Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Wrap trees and blobs in Python objects (just like commits were already\nwrapped), so that StGit code can read and write them.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/git.py |  139 ++++++++++++++++++++++++++++++++++++++++++++++--------\n 1 files changed, 118 insertions(+), 21 deletions(-)\n\n\ndiff --git a/stgit/lib/git.py b/stgit/lib/git.py\nindex f046e12..5bb1c93 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -146,13 +146,100 @@ class Person(Repr):\n                 defaults = cls.user())\n         return cls.__committer\n \n-class Tree(Repr):\n+class GitObject(Repr):\n+    \"\"\"Base class for all git objects.\"\"\"\n+\n+class Blobdata(Repr):\n+    def __init__(self, string):\n+        self.__string = str(string)\n+    str = property(lambda self: self.__string)\n+    def commit(self, repository):\n+        sha1 = repository.run(['git', 'hash-object', '-w', '--stdin']\n+                              ).raw_input(self.str).output_one_line()\n+        return repository.get_blob(sha1)\n+\n+class Blob(GitObject):\n     \"\"\"Immutable.\"\"\"\n-    def __init__(self, sha1):\n+    typename = 'blob'\n+    default_perm = '100644'\n+    def __init__(self, repository, sha1):\n+        self.__repository = repository\n         self.__sha1 = sha1\n     sha1 = property(lambda self: self.__sha1)\n     def __str__(self):\n-        return 'Tree<%s>' % self.sha1\n+        return 'Blob<%s>' % self.sha1\n+    @property\n+    def data(self):\n+        return Blobdata(self.__repository.cat_object(self.sha1))\n+\n+class ImmutableDict(dict):\n+    def error(*args, **kwargs):\n+        raise TypeError('Cannot modify immutable dict')\n+    __delitem__ = error\n+    __setitem__ = error\n+    clear = error\n+    pop = error\n+    popitem = error\n+    setdefault = error\n+    update = error\n+\n+class Treedata(Repr):\n+    \"\"\"Immutable.\"\"\"\n+    @staticmethod\n+    def __x(po):\n+        if isinstance(po, GitObject):\n+            perm, object = po.default_perm, po\n+        else:\n+            perm, object = po\n+        return perm, object\n+    def __init__(self, entries):\n+        self.__entries = ImmutableDict((name, self.__x(po))\n+                                       for (name, po) in entries.iteritems())\n+    entries = property(lambda self: self.__entries)\n+    def set_entry(self, name, po):\n+        e = dict(self.entries)\n+        e[name] = self.__x(po)\n+        return type(self)(e)\n+    def del_entry(self, name):\n+        e = dict(self.entries)\n+        del e[name]\n+        return type(self)(e)\n+    def commit(self, repository):\n+        listing = ''.join(\n+            '%s %s %s\\t%s\\0' % (mode, obj.typename, obj.sha1, name)\n+            for (name, (mode, obj)) in self.entries.iteritems())\n+        sha1 = repository.run(['git', 'mktree', '-z']\n+                              ).raw_input(listing).output_one_line()\n+        return repository.get_tree(sha1)\n+    @classmethod\n+    def parse(cls, repository, s):\n+        entries = {}\n+        for line in s.split('\\0')[:-1]:\n+            m = re.match(r'^([0-7]{6}) ([a-z]+) ([0-9a-f]{40})\\t(.*)$', line)\n+            assert m\n+            perm, type, sha1, name = m.groups()\n+            entries[name] = (perm, repository.get_object(type, sha1))\n+        return cls(entries)\n+\n+class Tree(GitObject):\n+    \"\"\"Immutable.\"\"\"\n+    typename = 'tree'\n+    default_perm = '040000'\n+    def __init__(self, repository, sha1):\n+        self.__sha1 = sha1\n+        self.__repository = repository\n+        self.__data = None\n+    sha1 = property(lambda self: self.__sha1)\n+    @property\n+    def data(self):\n+        if self.__data == None:\n+            self.__data = Treedata.parse(\n+                self.__repository,\n+                self.__repository.run(['git', 'ls-tree', '-z', self.sha1]\n+                                      ).raw_output())\n+        return self.__data\n+    def __str__(self):\n+        return 'Tree<sha1: %s>' % self.sha1\n \n class Commitdata(Repr):\n     \"\"\"Immutable.\"\"\"\n@@ -202,6 +289,22 @@ class Commitdata(Repr):\n         return ('Commitdata<tree: %s, parents: %s, author: %s,'\n                 ' committer: %s, message: \"%s\">'\n                 ) % (tree, parents, self.author, self.committer, self.message)\n+    def commit(self, repository):\n+        c = ['git', 'commit-tree', self.tree.sha1]\n+        for p in self.parents:\n+            c.append('-p')\n+            c.append(p.sha1)\n+        env = {}\n+        for p, v1 in ((self.author, 'AUTHOR'),\n+                       (self.committer, 'COMMITTER')):\n+            if p != None:\n+                for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),\n+                                 ('date', 'DATE')):\n+                    if getattr(p, attr) != None:\n+                        env['GIT_%s_%s' % (v1, v2)] = str(getattr(p, attr))\n+        sha1 = repository.run(c, env = env).raw_input(self.message\n+                                                      ).output_one_line()\n+        return repository.get_commit(sha1)\n     @classmethod\n     def parse(cls, repository, s):\n         cd = cls(parents = [])\n@@ -223,8 +326,9 @@ class Commitdata(Repr):\n                 assert False\n         assert False\n \n-class Commit(Repr):\n+class Commit(GitObject):\n     \"\"\"Immutable.\"\"\"\n+    typename = 'commit'\n     def __init__(self, repository, sha1):\n         self.__sha1 = sha1\n         self.__repository = repository\n@@ -306,7 +410,8 @@ class Repository(RunWithEnv):\n     def __init__(self, directory):\n         self.__git_dir = directory\n         self.__refs = Refs(self)\n-        self.__trees = ObjectCache(lambda sha1: Tree(sha1))\n+        self.__blobs = ObjectCache(lambda sha1: Blob(self, sha1))\n+        self.__trees = ObjectCache(lambda sha1: Tree(self, sha1))\n         self.__commits = ObjectCache(lambda sha1: Commit(self, sha1))\n         self.__default_index = None\n         self.__default_worktree = None\n@@ -357,26 +462,18 @@ class Repository(RunWithEnv):\n                     ).output_one_line())\n         except run.RunException:\n             raise RepositoryException('%s: No such revision' % rev)\n+    def get_blob(self, sha1):\n+        return self.__blobs[sha1]\n     def get_tree(self, sha1):\n         return self.__trees[sha1]\n     def get_commit(self, sha1):\n         return self.__commits[sha1]\n-    def commit(self, commitdata):\n-        c = ['git', 'commit-tree', commitdata.tree.sha1]\n-        for p in commitdata.parents:\n-            c.append('-p')\n-            c.append(p.sha1)\n-        env = {}\n-        for p, v1 in ((commitdata.author, 'AUTHOR'),\n-                       (commitdata.committer, 'COMMITTER')):\n-            if p != None:\n-                for attr, v2 in (('name', 'NAME'), ('email', 'EMAIL'),\n-                                 ('date', 'DATE')):\n-                    if getattr(p, attr) != None:\n-                        env['GIT_%s_%s' % (v1, v2)] = str(getattr(p, attr))\n-        sha1 = self.run(c, env = env).raw_input(commitdata.message\n-                                                ).output_one_line()\n-        return self.get_commit(sha1)\n+    def get_object(self, type, sha1):\n+        return { Blob.typename: self.get_blob,\n+                 Tree.typename: self.get_tree,\n+                 Commit.typename: self.get_commit }[type](sha1)\n+    def commit(self, objectdata):\n+        return objectdata.commit(self)\n     @property\n     def head(self):\n         try:\n"},{"id":"74841","messageId":"20080420221044.5837.25817.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 04/10] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:44Z","receivedAt":"2008-04-20T22:10:44Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Create a log branch (called <branchname>.stgit) for each StGit branch,\nand write to it whenever the stack is modified.\n\nCommands using the new infrastructure write to the log when they\ncommit a transaction. Commands using the old infrastructure get a log\nentry write written for them when they exit, unless they explicitly\nask for this not to happen.\n\nAs of yet, nothing can be done with this log.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/branch.py  |   20 +++++--\n stgit/commands/common.py  |    8 ++-\n stgit/commands/diff.py    |    2 -\n stgit/commands/export.py  |    2 -\n stgit/commands/files.py   |    2 -\n stgit/commands/id.py      |    2 -\n stgit/commands/log.py     |    2 -\n stgit/commands/mail.py    |    2 -\n stgit/commands/patches.py |    2 -\n stgit/commands/series.py  |    2 -\n stgit/commands/show.py    |    2 -\n stgit/commands/status.py  |    3 +\n stgit/commands/top.py     |    2 -\n stgit/lib/log.py          |  131 +++++++++++++++++++++++++++++++++++++++++++++\n stgit/lib/transaction.py  |    3 +\n stgit/main.py             |    2 +\n 16 files changed, 168 insertions(+), 19 deletions(-)\n create mode 100644 stgit/lib/log.py\n\n\ndiff --git a/stgit/commands/branch.py b/stgit/commands/branch.py\nindex 50684bb..241b1ef 100644\n--- a/stgit/commands/branch.py\n+++ b/stgit/commands/branch.py\n@@ -25,7 +25,7 @@ from stgit.commands.common import *\n from stgit.utils import *\n from stgit.out import *\n from stgit import stack, git, basedir\n-\n+from stgit.lib import log\n \n help = 'manage patch stacks'\n usage = \"\"\"%prog [options] branch-name [commit-id]\n@@ -40,7 +40,7 @@ When displaying the branches, the names can be prefixed with\n \n If not given any options, switch to the named branch.\"\"\"\n \n-directory = DirectoryGotoToplevel()\n+directory = DirectoryGotoToplevel(log = False)\n options = [make_option('-c', '--create',\n                        help = 'create a new development branch',\n                        action = 'store_true'),\n@@ -161,6 +161,7 @@ def func(parser, options, args):\n                                    parent_branch = parentbranch)\n \n         out.info('Branch \"%s\" created' % args[0])\n+        log.compat_log_entry('stg branch --create')\n         return\n \n     elif options.clone:\n@@ -181,6 +182,8 @@ def func(parser, options, args):\n         crt_series.clone(clone)\n         out.done()\n \n+        log.copy_log(log.default_repo(), crt_series.get_name(), clone,\n+                     'stg branch --clone')\n         return\n \n     elif options.delete:\n@@ -188,6 +191,7 @@ def func(parser, options, args):\n         if len(args) != 1:\n             parser.error('incorrect number of arguments')\n         __delete_branch(args[0], options.force)\n+        log.delete_log(log.default_repo(), args[0])\n         return\n \n     elif options.list:\n@@ -195,13 +199,16 @@ def func(parser, options, args):\n         if len(args) != 0:\n             parser.error('incorrect number of arguments')\n \n-        branches = git.get_heads()\n-        branches.sort()\n+        branches = set(git.get_heads())\n+        for br in set(branches):\n+            m = re.match(r'^(.*)\\.stgit$', br)\n+            if m and m.group(1) in branches:\n+                branches.remove(br)\n \n         if branches:\n             out.info('Available branches:')\n             max_len = max([len(i) for i in branches])\n-            for i in branches:\n+            for i in sorted(branches):\n                 __print_branch(i, max_len)\n         else:\n             out.info('No branches')\n@@ -238,7 +245,8 @@ def func(parser, options, args):\n         stack.Series(args[0]).rename(args[1])\n \n         out.info('Renamed branch \"%s\" to \"%s\"' % (args[0], args[1]))\n-\n+        log.rename_log(log.default_repo(), args[0], args[1],\n+                       'stg branch --rename')\n         return\n \n     elif options.unprotect:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex d6df813..1a45d9e 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -28,6 +28,7 @@ from stgit.run import *\n from stgit import stack, git, basedir\n from stgit.config import config, file_extensions\n from stgit.lib import stack as libstack\n+from stgit.lib import log\n \n # Command exception class\n class CmdException(StgException):\n@@ -478,8 +479,9 @@ class DirectoryException(StgException):\n     pass\n \n class _Directory(object):\n-    def __init__(self, needs_current_series = True):\n+    def __init__(self, needs_current_series = True, log = True):\n         self.needs_current_series =  needs_current_series\n+        self.log = log\n     @readonly_constant_property\n     def git_dir(self):\n         try:\n@@ -512,6 +514,9 @@ class _Directory(object):\n                        ).output_one_line()]\n     def cd_to_topdir(self):\n         os.chdir(self.__topdir_path)\n+    def write_log(self, msg):\n+        if self.log:\n+            log.compat_log_entry(msg)\n \n class DirectoryAnywhere(_Directory):\n     def setup(self):\n@@ -536,6 +541,7 @@ class DirectoryHasRepositoryLib(_Directory):\n     \"\"\"For commands that use the new infrastructure in stgit.lib.*.\"\"\"\n     def __init__(self):\n         self.needs_current_series = False\n+        self.log = False # stgit.lib.transaction handles logging\n     def setup(self):\n         # This will throw an exception if we don't have a repository.\n         self.repository = libstack.Repository.default()\ndiff --git a/stgit/commands/diff.py b/stgit/commands/diff.py\nindex fd6be34..8966642 100644\n--- a/stgit/commands/diff.py\n+++ b/stgit/commands/diff.py\n@@ -42,7 +42,7 @@ rev = '([patch][//[bottom | top]]) | <tree-ish> | base'\n If neither bottom nor top are given but a '//' is present, the command\n shows the specified patch (defaulting to the current one).\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-r', '--range',\n                        metavar = 'rev1[..[rev2]]', dest = 'revs',\n                        help = 'show the diff between revisions'),\ndiff --git a/stgit/commands/export.py b/stgit/commands/export.py\nindex 50f6f67..552fd44 100644\n--- a/stgit/commands/export.py\n+++ b/stgit/commands/export.py\n@@ -49,7 +49,7 @@ file:\n   %(commemail)s   - committer's e-mail\n \"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-d', '--dir',\n                        help = 'export patches to DIR instead of the default'),\n            make_option('-p', '--patch',\ndiff --git a/stgit/commands/files.py b/stgit/commands/files.py\nindex b43b12f..7844f8d 100644\n--- a/stgit/commands/files.py\n+++ b/stgit/commands/files.py\n@@ -34,7 +34,7 @@ given patch. Note that this command doesn't show the files modified in\n the working tree and not yet included in the patch by a 'refresh'\n command. Use the 'diff' or 'status' commands for these files.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-s', '--stat',\n                        help = 'show the diff stat',\n                        action = 'store_true'),\ndiff --git a/stgit/commands/id.py b/stgit/commands/id.py\nindex 94b0229..5bb1ad2 100644\n--- a/stgit/commands/id.py\n+++ b/stgit/commands/id.py\n@@ -33,7 +33,7 @@ the standard GIT id's like heads and tags, this command also accepts\n 'top' or 'bottom' are passed and <patch> is a valid patch name, 'top'\n will be used by default.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one')]\n \ndiff --git a/stgit/commands/log.py b/stgit/commands/log.py\nindex 52d55a5..13e0baa 100644\n--- a/stgit/commands/log.py\n+++ b/stgit/commands/log.py\n@@ -44,7 +44,7 @@ represent the changes to the entire base of the current\n patch. Conflicts reset the patch content and a subsequent 'refresh'\n will show the entire patch.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one'),\n            make_option('-p', '--patch',\ndiff --git a/stgit/commands/mail.py b/stgit/commands/mail.py\nindex b4d4e18..4027170 100644\n--- a/stgit/commands/mail.py\n+++ b/stgit/commands/mail.py\n@@ -90,7 +90,7 @@ the following:\n   %(prefix)s       - 'prefix ' string passed on the command line\n   %(shortdescr)s   - the first line of the patch description\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-a', '--all',\n                        help = 'e-mail all the applied patches',\n                        action = 'store_true'),\ndiff --git a/stgit/commands/patches.py b/stgit/commands/patches.py\nindex 140699d..c95c40f 100644\n--- a/stgit/commands/patches.py\n+++ b/stgit/commands/patches.py\n@@ -33,7 +33,7 @@ it shows the patches affected by the local tree modifications. The\n '--diff' option also lists the patch log and the diff for the given\n files.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-d', '--diff',\n                        help = 'show the diff for the given files',\n                        action = 'store_true'),\ndiff --git a/stgit/commands/series.py b/stgit/commands/series.py\nindex e3467cc..8d19dd7 100644\n--- a/stgit/commands/series.py\n+++ b/stgit/commands/series.py\n@@ -34,7 +34,7 @@ range. The applied patches are prefixed with a '+', the unapplied ones\n with a '-' and the hidden ones with a '!'. The current patch is\n prefixed with a '>'. Empty patches are prefixed with a '0'.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one'),\n            make_option('-a', '--all',\ndiff --git a/stgit/commands/show.py b/stgit/commands/show.py\nindex b77a9c8..dd2a3a3 100644\n--- a/stgit/commands/show.py\n+++ b/stgit/commands/show.py\n@@ -30,7 +30,7 @@ Show the commit log and the diff corresponding to the given\n patches. The output is similar to that generated by the 'git show'\n command.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one'),\n            make_option('-a', '--applied',\ndiff --git a/stgit/commands/status.py b/stgit/commands/status.py\nindex 6da4516..37b79c0 100644\n--- a/stgit/commands/status.py\n+++ b/stgit/commands/status.py\n@@ -40,7 +40,7 @@ under revision control. The files are prefixed as follows:\n A 'refresh' command clears the status of the modified, new and deleted\n files.\"\"\"\n \n-directory = DirectoryHasRepository(needs_current_series = False)\n+directory = DirectoryHasRepository(needs_current_series = False, log = False)\n options = [make_option('-m', '--modified',\n                        help = 'show modified files only',\n                        action = 'store_true'),\n@@ -106,6 +106,7 @@ def func(parser, options, args):\n     directory.cd_to_topdir()\n \n     if options.reset:\n+        directory.log = True\n         if args:\n             conflicts = git.get_conflicts()\n             git.resolved(fn for fn in args if fn in conflicts)\ndiff --git a/stgit/commands/top.py b/stgit/commands/top.py\nindex e7cb275..96d680e 100644\n--- a/stgit/commands/top.py\n+++ b/stgit/commands/top.py\n@@ -30,7 +30,7 @@ usage = \"\"\"%prog [options]\n \n Print the name of the current (topmost) patch.\"\"\"\n \n-directory = DirectoryHasRepository()\n+directory = DirectoryHasRepository(log = False)\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one')]\n \ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nnew file mode 100644\nindex 0000000..fac050f\n--- /dev/null\n+++ b/stgit/lib/log.py\n@@ -0,0 +1,131 @@\n+from stgit.lib import git, stack\n+from stgit import exception\n+from stgit.out import out\n+\n+class LogException(exception.StgException):\n+    pass\n+\n+_current_version = 1\n+\n+def commit_info(cd):\n+    return 'Author: %s\\n\\n%s' % (cd.author, cd.message)\n+\n+def patch_tree(repository, cd):\n+    return repository.commit(git.Treedata({\n+                'a': cd.parent.data.tree, 'b': cd.tree,\n+                'info': repository.commit(git.Blobdata(commit_info(cd))) }))\n+\n+def order_blob(repository, stack, kind):\n+    return repository.commit(git.Blobdata(''.join(\n+                '%s %s\\n' % (stack.patches.get(pn).commit.sha1, pn)\n+                for pn in getattr(stack.patchorder, kind))))\n+\n+def log_entry_tree(repository, stack):\n+    patches = repository.commit(\n+        git.Treedata(dict((pn, patch_tree(repository,\n+                                          stack.patches.get(pn).commit.data))\n+                          for pn in stack.patchorder.all)))\n+    return repository.commit(git.Treedata({\n+                'version': repository.commit(git.Blobdata(\n+                        str(_current_version))),\n+                'head': repository.commit(git.Blobdata(str(stack.head.sha1))),\n+                'patches': patches,\n+                'applied': order_blob(repository, stack, 'applied'),\n+                'unapplied': order_blob(repository, stack, 'unapplied'),\n+                }))\n+\n+def log_ref(branch):\n+    return 'refs/heads/%s.stgit' % branch\n+\n+def log_entry(stack, msg):\n+    \"\"\"Write a new log entry for the stack.\"\"\"\n+    ref = log_ref(stack.name)\n+    try:\n+        last_log = [FullLog(stack.repository, ref,\n+                            stack.repository.refs.get(ref))]\n+    except KeyError:\n+        last_log = []\n+    except LogException, e:\n+        out.warn(str(e), 'No log entry written.')\n+        return\n+    log_tree = log_entry_tree(stack.repository, stack)\n+    stack_log = stack.repository.commit(\n+        git.Commitdata(tree = log_tree, message = msg,\n+                       parents = [ll.stack_log for ll in last_log]))\n+    full_log = stack.repository.commit(\n+        git.Commitdata(tree = log_tree, message = msg,\n+                       parents = ([stack_log] + [stack.head]\n+                                  + [ll.full_log for ll in last_log])))\n+    stack.repository.refs.set(ref, full_log, msg)\n+\n+def compat_log_entry(msg):\n+    repo = default_repo()\n+    stack = repo.get_stack(repo.current_branch)\n+    log_entry(stack, msg)\n+\n+class Log(object):\n+    \"\"\"Read a log entry.\"\"\"\n+    def __init__(self, repo, ref, commit):\n+        self.commit = commit\n+        mode, vblob = self.commit.data.tree.data.entries.get(\n+            'version', (None, None))\n+        if not isinstance(vblob, git.Blob):\n+            raise LogException('%s does not contain a valid log' % ref)\n+        try:\n+            version = int(vblob.data.str)\n+        except ValueError:\n+            raise LogException('\"%s\": invalid version number' % vblob.data.str)\n+        if version < _current_version:\n+            raise LogException(\n+                '%s contains a stack log older than version %d;'\n+                ' please delete it' % (ref, _current_version))\n+        elif version > _current_version:\n+            raise LogException(\n+                'Log contains a stack log newer than version %d;'\n+                ' this version of StGit cannot read it' % _current_version)\n+\n+        # TODO: read the rest of the log lazily.\n+\n+        def pl(name):\n+            patches = [x.split() for x in\n+                       self.commit.data.tree.data.entries[name][1]\n+                       .data.str.strip().split('\\n') if x]\n+            # TODO: handle case where we don't have the commit object.\n+            return ([pn for sha1, pn in patches],\n+                    dict((pn, repo.get_commit(sha1)) for sha1, pn in patches))\n+        self.patches = {}\n+        self.applied, patches = pl('applied')\n+        self.patches.update(patches)\n+        self.unapplied, patches = pl('unapplied')\n+        self.patches.update(patches)\n+        self.head = repo.get_commit(\n+            self.commit.data.tree.data.entries['head'][1].data.str)\n+        if self.applied:\n+            self.base = self.patches[self.applied[0]].data.parent\n+        else:\n+            self.base = self.head\n+\n+class FullLog(Log):\n+    full_log = property(lambda self: self.commit)\n+    stack_log = property(lambda self: self.commit.data.parents[0])\n+\n+def delete_log(repo, branch):\n+    ref = log_ref(branch)\n+    if repo.refs.exists(ref):\n+        repo.refs.delete(ref)\n+\n+def rename_log(repo, old_branch, new_branch, msg):\n+    old_ref = log_ref(old_branch)\n+    new_ref = log_ref(new_branch)\n+    if repo.refs.exists(old_ref):\n+        repo.refs.set(new_ref, repo.refs.get(old_ref), msg)\n+        repo.refs.delete(old_ref)\n+\n+def copy_log(repo, src_branch, dst_branch, msg):\n+    src_ref = log_ref(src_branch)\n+    dst_ref = log_ref(dst_branch)\n+    if repo.refs.exists(src_ref):\n+        repo.refs.set(dst_ref, repo.refs.get(src_ref), msg)\n+\n+def default_repo():\n+    return stack.Repository.default()\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 874f81b..4fb37ce 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -1,7 +1,7 @@\n from stgit import exception, utils\n from stgit.utils import any, all\n from stgit.out import *\n-from stgit.lib import git\n+from stgit.lib import git, log\n \n class TransactionException(exception.StgException):\n     pass\n@@ -131,6 +131,7 @@ class StackTransaction(object):\n         _print_current_patch(self.__stack.patchorder.applied, self.__applied)\n         self.__stack.patchorder.applied = self.__applied\n         self.__stack.patchorder.unapplied = self.__unapplied\n+        log.log_entry(self.__stack, self.__msg)\n \n         if self.__error:\n             return utils.STGIT_CONFLICT\ndiff --git a/stgit/main.py b/stgit/main.py\nindex aa1f8ef..bd1a187 100644\n--- a/stgit/main.py\n+++ b/stgit/main.py\n@@ -277,6 +277,7 @@ def main():\n \n         ret = command.func(parser, options, args)\n     except (StgException, IOError, ParsingError, NoSectionError), err:\n+        directory.write_log('%s %s' % (prog, cmd))\n         out.error(str(err), title = '%s %s' % (prog, cmd))\n         if debug_level > 0:\n             traceback.print_exc()\n@@ -292,4 +293,5 @@ def main():\n         traceback.print_exc()\n         sys.exit(utils.STGIT_BUG_ERROR)\n \n+    directory.write_log('%s %s' % (prog, cmd))\n     sys.exit(ret or utils.STGIT_SUCCESS)\n"},{"id":"74840","messageId":"20080420221050.5837.35826.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 05/10] Add utility function for reordering patches","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:50Z","receivedAt":"2008-04-20T22:10:50Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Signed-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/transaction.py |   14 ++++++++++++++\n 1 files changed, 14 insertions(+), 0 deletions(-)\n\n\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 4fb37ce..4c3b448 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -1,3 +1,5 @@\n+import itertools as it\n+\n from stgit import exception, utils\n from stgit.utils import any, all\n from stgit.out import *\n@@ -233,3 +235,15 @@ class StackTransaction(object):\n             self.__allow_conflicts = lambda trans: True\n \n             self.__halt('Merge conflict')\n+\n+    def reorder_patches(self, applied, unapplied, iw = None):\n+        \"\"\"Push and pop patches to attain the given ordering.\"\"\"\n+        common = len(list(it.takewhile(lambda (a, b): a == b,\n+                                       zip(self.applied, applied))))\n+        to_pop = set(self.applied[common:])\n+        self.pop_patches(lambda pn: pn in to_pop)\n+        for pn in applied[common:]:\n+            self.push_patch(pn, iw)\n+        assert self.applied == applied\n+        assert set(self.unapplied) == set(unapplied)\n+        self.unapplied = unapplied\n"},{"id":"74843","messageId":"20080420221055.5837.81513.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 06/10] New command: stg reset","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:10:55Z","receivedAt":"2008-04-20T22:10:55Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Given a commit object from the log, resets the stack (or just the\nnamed patches) to the state given by that log entry.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/reset.py |  114 +++++++++++++++++++++++++++++++++++\n stgit/main.py           |    2 +\n t/t3100-reset.sh        |  151 +++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 267 insertions(+), 0 deletions(-)\n create mode 100644 stgit/commands/reset.py\n create mode 100755 t/t3100-reset.sh\n\n\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nnew file mode 100644\nindex 0000000..e27f440\n--- /dev/null\n+++ b/stgit/commands/reset.py\n@@ -0,0 +1,114 @@\n+# -*- coding: utf-8 -*-\n+\n+__copyright__ = \"\"\"\n+Copyright (C) 2008, Karl Hasselström <kha@treskal.com>\n+\n+This program is free software; you can redistribute it and/or modify\n+it under the terms of the GNU General Public License version 2 as\n+published by the Free Software Foundation.\n+\n+This program is distributed in the hope that it will be useful,\n+but WITHOUT ANY WARRANTY; without even the implied warranty of\n+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+GNU General Public License for more details.\n+\n+You should have received a copy of the GNU General Public License\n+along with this program; if not, write to the Free Software\n+Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n+\"\"\"\n+\n+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 <state> [<patchnames>]\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+\n+directory = common.DirectoryHasRepositoryLib()\n+options = []\n+\n+def reset_stack(stack, 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(stack.patchorder.all)\n+    to_delete = mask(existing_patches - patches_to_reset)\n+    trans = transaction.StackTransaction(stack, 'stg 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 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+        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+    return reset_stack(stack, stack.repository.default_iw, state, patches)\ndiff --git a/stgit/main.py b/stgit/main.py\nindex bd1a187..948f923 100644\n--- a/stgit/main.py\n+++ b/stgit/main.py\n@@ -89,6 +89,7 @@ commands = Commands({\n     'refresh':          'refresh',\n     'rename':           'rename',\n     'repair':           'repair',\n+    'reset':            'reset',\n     'resolved':         'resolved',\n     'series':           'series',\n     'show':             'show',\n@@ -122,6 +123,7 @@ stackcommands = (\n     'push',\n     'rebase',\n     'repair',\n+    'reset',\n     'series',\n     'sink',\n     'top',\ndiff --git a/t/t3100-reset.sh b/t/t3100-reset.sh\nnew file mode 100755\nindex 0000000..1805091\n--- /dev/null\n+++ b/t/t3100-reset.sh\n@@ -0,0 +1,151 @@\n+#!/bin/sh\n+\n+test_description='Simple test cases for \"stg reset\"'\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 reset master.stgit^~1 &&\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+333\n+EOF\n+test_expect_success 'Push one patch ...' '\n+    stg push &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2 p3\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\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 reset master.stgit^~1 &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_expect_success 'Commit one patch ...' '\n+    stg commit &&\n+    test \"$(echo $(stg applied))\" = \"p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\"\n+'\n+\n+test_expect_success '... and undo it' '\n+    stg reset master.stgit^~1 &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\"\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+EOF\n+test_expect_success 'Delete two patches ...' '\n+    stg delete p2 p3 &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_expect_success '... and undo one of the deletions ...' '\n+    stg reset master.stgit^~1 p3 &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_expect_success '... then undo the first undo ...' '\n+    stg reset master.stgit^~1 &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\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 the other deletion' '\n+    stg reset master.stgit^~3 p2 &&\n+    stg push p2 &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\n+    test_cmp expected.txt a\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+222\n+ggg\n+EOF\n+test_expect_success 'Refresh a patch ...' '\n+    echo ggg >> a &&\n+    stg refresh &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\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 the refresh' '\n+    stg reset master.stgit^~1 &&\n+    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_done\n"},{"id":"74842","messageId":"20080420221101.5837.13630.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 07/10] Log conflicts separately","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:11:01Z","receivedAt":"2008-04-20T22:11:01Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"This patch makes commands that produce a conflict log that final\nconflicting push separately from the rest of the command's effects.\nThis makes it possible for the user to roll back just the final\nconflicting push if she desires. (Rolling back the whole operation is\nof course still possible, by resetting to the state yet another step\nback in the log.)\n\nThis change only applies to the new-infrastructure commands.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/transaction.py |   47 +++++++++++++++++++++++++++++++---------------\n 1 files changed, 32 insertions(+), 15 deletions(-)\n\n\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 4c3b448..92bcfd5 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -42,6 +42,7 @@ class StackTransaction(object):\n         self.__patches = _TransPatchMap(stack)\n         self.__applied = list(self.__stack.patchorder.applied)\n         self.__unapplied = list(self.__stack.patchorder.unapplied)\n+        self.__conflicting_push = None\n         self.__error = None\n         self.__current_tree = self.__stack.head.data.tree\n         self.__base = self.__stack.base\n@@ -121,19 +122,26 @@ class StackTransaction(object):\n             out.error(self.__error)\n \n         # Write patches.\n-        for pn, commit in self.__patches.iteritems():\n-            if self.__stack.patches.exists(pn):\n-                p = self.__stack.patches.get(pn)\n-                if commit == None:\n-                    p.delete()\n+        def write(msg):\n+            for pn, commit in self.__patches.iteritems():\n+                if self.__stack.patches.exists(pn):\n+                    p = self.__stack.patches.get(pn)\n+                    if commit == None:\n+                        p.delete()\n+                    else:\n+                        p.set_commit(commit, msg)\n                 else:\n-                    p.set_commit(commit, self.__msg)\n-            else:\n-                self.__stack.patches.new(pn, commit, self.__msg)\n-        _print_current_patch(self.__stack.patchorder.applied, self.__applied)\n-        self.__stack.patchorder.applied = self.__applied\n-        self.__stack.patchorder.unapplied = self.__unapplied\n-        log.log_entry(self.__stack, self.__msg)\n+                    self.__stack.patches.new(pn, commit, msg)\n+            self.__stack.patchorder.applied = self.__applied\n+            self.__stack.patchorder.unapplied = self.__unapplied\n+            log.log_entry(self.__stack, msg)\n+        old_applied = self.__stack.patchorder.applied\n+        write(self.__msg)\n+        if self.__conflicting_push != None:\n+            self.__patches = _TransPatchMap(self.__stack)\n+            self.__conflicting_push()\n+            write(self.__msg + ' (CONFLICT)')\n+        _print_current_patch(old_applied, self.__applied)\n \n         if self.__error:\n             return utils.STGIT_CONFLICT\n@@ -223,18 +231,27 @@ class StackTransaction(object):\n         cd = cd.set_tree(tree)\n         if any(getattr(cd, a) != getattr(orig_cd, a) for a in\n                ['parent', 'tree', 'author', 'message']):\n-            self.patches[pn] = self.__stack.repository.commit(cd)\n+            comm = self.__stack.repository.commit(cd)\n         else:\n+            comm = None\n             s = ' (unmodified)'\n-        del self.unapplied[self.unapplied.index(pn)]\n-        self.applied.append(pn)\n         out.info('Pushed %s%s' % (pn, s))\n+        def update():\n+            if comm:\n+                self.patches[pn] = comm\n+            del self.unapplied[self.unapplied.index(pn)]\n+            self.applied.append(pn)\n         if merge_conflict:\n             # We've just caused conflicts, so we must allow them in\n             # the final checkout.\n             self.__allow_conflicts = lambda trans: True\n \n+            # Save this update so that we can run it a little later.\n+            self.__conflicting_push = update\n             self.__halt('Merge conflict')\n+        else:\n+            # Update immediately.\n+            update()\n \n     def reorder_patches(self, applied, unapplied, iw = None):\n         \"\"\"Push and pop patches to attain the given ordering.\"\"\"\n"},{"id":"74844","messageId":"20080420221107.5837.46522.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 08/10] Log conflicts separately for all commands","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:11:07Z","receivedAt":"2008-04-20T22:11:07Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"This takes care of the old-infrastructure commands as well. They'll\nall be converted to the new infrastructure eventually, but until then\nthis patch is necessary to make all the commands behave consistently.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/log.py   |   29 ++++++++++++++++++++++++++++-\n stgit/lib/stack.py |    7 +++++++\n 2 files changed, 35 insertions(+), 1 deletions(-)\n\n\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex fac050f..9904941 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -58,10 +58,37 @@ def log_entry(stack, msg):\n                                   + [ll.full_log for ll in last_log])))\n     stack.repository.refs.set(ref, full_log, msg)\n \n+class Fakestack(object):\n+    \"\"\"Imitates a real Stack, but with the topmost patch popped.\"\"\"\n+    def __init__(self, stack):\n+        appl = list(stack.patchorder.applied)\n+        unappl = list(stack.patchorder.unapplied)\n+        class patchorder(object):\n+            applied = appl[:-1]\n+            unapplied = [appl[-1]] + unappl\n+            all = appl + unappl\n+        self.patchorder = patchorder\n+        class patches(object):\n+            @staticmethod\n+            def get(pn):\n+                if pn == appl[-1]:\n+                    class patch(object):\n+                        commit = stack.patches.get(pn).old_commit\n+                    return patch\n+                else:\n+                    return stack.patches.get(pn)\n+        self.patches = patches\n+        self.head = stack.head.data.parent\n+        self.name = stack.name\n+        self.repository = stack.repository\n def compat_log_entry(msg):\n     repo = default_repo()\n     stack = repo.get_stack(repo.current_branch)\n-    log_entry(stack, msg)\n+    if repo.default_index.conflicts():\n+        log_entry(Fakestack(stack), msg)\n+        log_entry(stack, msg + ' (CONFLICT)')\n+    else:\n+        log_entry(stack, msg)\n \n class Log(object):\n     \"\"\"Read a log entry.\"\"\"\ndiff --git a/stgit/lib/stack.py b/stgit/lib/stack.py\nindex af1c994..95b817f 100644\n--- a/stgit/lib/stack.py\n+++ b/stgit/lib/stack.py\n@@ -17,6 +17,13 @@ class Patch(object):\n     def commit(self):\n         return self.__stack.repository.refs.get(self.__ref)\n     @property\n+    def old_commit(self):\n+        \"\"\"Return the previous commit for this patch.\"\"\"\n+        fn = os.path.join(self.__compat_dir, 'top.old')\n+        if not os.path.isfile(fn):\n+            return None\n+        return self.__stack.repository.get_commit(utils.read_string(fn))\n+    @property\n     def __compat_dir(self):\n         return os.path.join(self.__stack.directory, 'patches', self.__name)\n     def __write_compat_files(self, new_commit, msg):\n"},{"id":"74845","messageId":"20080420221112.5837.73514.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 09/10] Add a --hard flag to stg reset","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:11:13Z","receivedAt":"2008-04-20T22:11:13Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"With this flag, reset will overwrite any local changes. Useful e.g.\nwhen undoing a push that's polluted the index+worktree with a heap of\nconflicts.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/reset.py  |   14 +++++++-----\n stgit/lib/git.py         |    4 +++\n stgit/lib/transaction.py |   11 +++++++--\n t/t3101-reset-hard.sh    |   56 ++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 77 insertions(+), 8 deletions(-)\n create mode 100755 t/t3101-reset-hard.sh\n\n\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nindex e27f440..00226f0 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/reset.py\n@@ -17,12 +17,13 @@ along with this program; if not, write to the Free Software\n Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\n \"\"\"\n \n+from optparse import make_option\n 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 <state> [<patchnames>]\n+usage = \"\"\"%prog [options] <state> [<patchnames>]\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@@ -41,9 +42,10 @@ If one or more patch names are given, reset only those patches, and\n leave the rest alone.\"\"\"\n \n directory = common.DirectoryHasRepositoryLib()\n-options = []\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):\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@@ -53,7 +55,8 @@ def reset_stack(stack, iw, state, only_patches):\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, 'stg reset')\n+    trans = transaction.StackTransaction(stack, 'stg reset',\n+                                         discard_changes = hard)\n \n     # If we have to change the stack base, we need to pop all patches\n     # first.\n@@ -111,4 +114,5 @@ 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+    return reset_stack(stack, stack.repository.default_iw, state, patches,\n+                       options.hard)\ndiff --git a/stgit/lib/git.py b/stgit/lib/git.py\nindex 5bb1c93..c044b46 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -605,6 +605,10 @@ class IndexAndWorktree(RunWithEnvCwd):\n     env = property(lambda self: utils.add_dict(self.__index.env,\n                                                self.__worktree.env))\n     cwd = property(lambda self: self.__worktree.directory)\n+    def checkout_hard(self, tree):\n+        assert isinstance(tree, Tree)\n+        self.run(['git', 'read-tree', '--reset', '-u', tree.sha1]\n+                 ).discard_output()\n     def checkout(self, old_tree, new_tree):\n         # TODO: Optionally do a 3-way instead of doing nothing when we\n         # have a problem. Or maybe we should stash changes in a patch?\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 92bcfd5..a008780 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -36,7 +36,8 @@ class _TransPatchMap(dict):\n             return self.__stack.patches.get(pn).commit\n \n class StackTransaction(object):\n-    def __init__(self, stack, msg, allow_conflicts = False):\n+    def __init__(self, stack, msg, discard_changes = False,\n+                 allow_conflicts = False):\n         self.__stack = stack\n         self.__msg = msg\n         self.__patches = _TransPatchMap(stack)\n@@ -46,6 +47,7 @@ class StackTransaction(object):\n         self.__error = None\n         self.__current_tree = self.__stack.head.data.tree\n         self.__base = self.__stack.base\n+        self.__discard_changes = discard_changes\n         if isinstance(allow_conflicts, bool):\n             self.__allow_conflicts = lambda trans: allow_conflicts\n         else:\n@@ -70,7 +72,7 @@ class StackTransaction(object):\n                 'This can happen if you modify a branch with git.',\n                 '\"stg repair --help\" explains more about what to do next.')\n             self.__abort()\n-        if self.__current_tree == tree:\n+        if self.__current_tree == tree and not self.__discard_changes:\n             # No tree change, but we still want to make sure that\n             # there are no unresolved conflicts. Conflicts\n             # conceptually \"belong\" to the topmost patch, and just\n@@ -81,7 +83,10 @@ class StackTransaction(object):\n             out.error('Need to resolve conflicts first')\n             self.__abort()\n         assert iw != None\n-        iw.checkout(self.__current_tree, tree)\n+        if self.__discard_changes:\n+            iw.checkout_hard(tree)\n+        else:\n+            iw.checkout(self.__current_tree, tree)\n         self.__current_tree = tree\n     @staticmethod\n     def __abort():\ndiff --git a/t/t3101-reset-hard.sh b/t/t3101-reset-hard.sh\nnew file mode 100755\nindex 0000000..1e02805\n--- /dev/null\n+++ b/t/t3101-reset-hard.sh\n@@ -0,0 +1,56 @@\n+#!/bin/sh\n+\n+test_description='Simple test cases for \"stg reset\"'\n+\n+. ./test-lib.sh\n+\n+# Ignore our own output files.\n+cat > .git/info/exclude <<EOF\n+/expected.txt\n+/actual.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+'\n+\n+cat > expected.txt <<EOF\n+C a\n+EOF\n+test_expect_success 'Pop middle patch, creating a conflict' '\n+    ! stg pop p2 &&\n+    stg status a > actual.txt &&\n+    test_cmp expected.txt actual.txt &&\n+    test \"$(echo $(stg applied))\" = \"p1 p3\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p2\"\n+'\n+\n+test_expect_success 'Try to reset without --hard' '\n+    ! stg reset master.stgit^~1 &&\n+    stg status a > actual.txt &&\n+    test_cmp expected.txt actual.txt &&\n+    test \"$(echo $(stg applied))\" = \"p1 p3\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p2\"\n+'\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+    stg status a > actual.txt &&\n+    test_cmp expected.txt actual.txt &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3 p2\"\n+'\n+\n+test_done\n"},{"id":"74846","messageId":"20080420221118.5837.38770.stgit@yoghurt","threadId":"13191","inReplyTo":"20080420215625.5837.82896.stgit@yoghurt","subject":"[StGit PATCH 10/10] Don't write a log entry if there were no changes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-04-20T22:11:18Z","receivedAt":"2008-04-20T22:11:18Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Some commands end up calling log_entry() without verifying that they\ndid in fact change anything. (One example of this is a conflicting\npush, which will log two entries, everything else and the conflicting\npush, with the \"everything else\" part being empty if there was only\none patch to push.) So before appending to the log, make sure that the\nentry we're appending isn't a no-op.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/log.py |    3 +++\n 1 files changed, 3 insertions(+), 0 deletions(-)\n\n\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex 9904941..a18c80e 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -49,6 +49,9 @@ def log_entry(stack, msg):\n         out.warn(str(e), 'No log entry written.')\n         return\n     log_tree = log_entry_tree(stack.repository, stack)\n+    if len(last_log) == 1 and log_tree == last_log[0].full_log.data.tree:\n+        # No changes, so there's no point writing a new log entry.\n+        return\n     stack_log = stack.repository.commit(\n         git.Commitdata(tree = log_tree, message = msg,\n                        parents = [ll.stack_log for ll in last_log]))\n"}]}