{"thread":{"id":"13908","subject":"[StGit PATCH 01/14] Fix typo","startedAt":"2008-06-12T05:34:04Z","lastAt":"2008-07-14T06:32:56Z","messageCount":32,"participants":["Karl Hasselström","Catalin Marinas"],"isPatch":true,"patchVersion":1,"patchTotal":14},"messages":[{"id":"79544","messageId":"20080612052913.23549.69687.stgit@yoghurt","threadId":"13908","inReplyTo":null,"subject":"[StGit PATCH 00/14] Undo series","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:04Z","receivedAt":"2008-06-12T05:34:04Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Now with API documentation added all over the place! Especially in\n2/14 and 3/14.\n\nThe only two things missing before I'll ask for this to be included\nare\n\n  1. Conversion of stg refresh to the new infrastructure, so that it\n     can write two separate steps to the stack log (which will allow\n     undo et.al. to handle it nicely).\n\n  2. Log writing optimization: Right now, we always write all the\n     patches to the log, without trying to reuse the entries for the\n     ones that haven't changed since last time.\n\n---\n\nKarl Hasselström (14):\n      Make \"stg log\" show stack log instead of patch log\n      Log and undo external modifications\n      New command: stg redo\n      New command: stg undo\n      Move stack reset function to a shared location\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      Fix typo\n\n\n stgit/commands/branch.py     |   19 +-\n stgit/commands/common.py     |    9 +\n stgit/commands/diff.py       |    2 \n stgit/commands/files.py      |    2 \n stgit/commands/id.py         |    2 \n stgit/commands/log.py        |  169 +++++------------\n stgit/commands/mail.py       |    2 \n stgit/commands/patches.py    |    2 \n stgit/commands/redo.py       |   52 +++++\n stgit/commands/reset.py      |   56 +++++\n stgit/commands/show.py       |    2 \n stgit/commands/status.py     |    3 \n stgit/commands/undo.py       |   49 +++++\n stgit/lib/git.py             |  197 +++++++++++++++++--\n stgit/lib/log.py             |  429 ++++++++++++++++++++++++++++++++++++++++++\n stgit/lib/stack.py           |   16 ++\n stgit/lib/transaction.py     |  121 +++++++++---\n stgit/main.py                |    8 +\n t/t1400-patch-history.sh     |  103 ----------\n t/t3100-reset.sh             |  151 +++++++++++++++\n t/t3101-reset-hard.sh        |   56 +++++\n t/t3102-undo.sh              |   86 ++++++++\n t/t3103-undo-hard.sh         |   56 +++++\n t/t3104-redo.sh              |  122 ++++++++++++\n t/t3105-undo-external-mod.sh |   68 +++++++\n 25 files changed, 1491 insertions(+), 291 deletions(-)\n create mode 100644 stgit/commands/redo.py\n create mode 100644 stgit/commands/reset.py\n create mode 100644 stgit/commands/undo.py\n create mode 100644 stgit/lib/log.py\n delete mode 100755 t/t1400-patch-history.sh\n create mode 100755 t/t3100-reset.sh\n create mode 100755 t/t3101-reset-hard.sh\n create mode 100755 t/t3102-undo.sh\n create mode 100755 t/t3103-undo-hard.sh\n create mode 100755 t/t3104-redo.sh\n create mode 100755 t/t3105-undo-external-mod.sh\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"79543","messageId":"20080612053412.23549.4915.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 01/14] Fix typo","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:12Z","receivedAt":"2008-06-12T05:34:12Z","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/git.py |    2 +-\n 1 files changed, 1 insertions(+), 1 deletions(-)\n\n\ndiff --git a/stgit/lib/git.py b/stgit/lib/git.py\nindex 0e0cccb..6ccdfa7 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -20,7 +20,7 @@ class Immutable(object):\n     creating a whole new commit object that's exactly like the old one\n     except for the commit message.)\n \n-    The L{Immutable} class doesn't acytually enforce immutability --\n+    The L{Immutable} class doesn't actually enforce immutability --\n     that is up to the individual immutable subclasses. It just serves\n     as documentation.\"\"\"\n \n"},{"id":"79547","messageId":"20080612053418.23549.1299.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 02/14] Library functions for tree and blob manipulation","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:18Z","receivedAt":"2008-06-12T05:34:18Z","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 |  188 +++++++++++++++++++++++++++++++++++++++++++++++-------\n 1 files changed, 165 insertions(+), 23 deletions(-)\n\n\ndiff --git a/stgit/lib/git.py b/stgit/lib/git.py\nindex 6ccdfa7..a8881f4 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -182,16 +182,142 @@ class Person(Immutable, Repr):\n                 defaults = cls.user())\n         return cls.__committer\n \n-class Tree(Immutable, Repr):\n-    \"\"\"Represents a git tree object.\"\"\"\n-    def __init__(self, sha1):\n+class GitObject(Immutable, Repr):\n+    \"\"\"Base class for all git objects. One git object is represented by at\n+    most one C{GitObject}, which makes it possible to compare them\n+    using normal Python object comparison; it also ensures we don't\n+    waste more memory than necessary.\"\"\"\n+\n+class BlobData(Immutable, Repr):\n+    \"\"\"Represents the data contents of a git blob object.\"\"\"\n+    def __init__(self, string):\n+        self.__string = str(string)\n+    str = property(lambda self: self.__string)\n+    def commit(self, repository):\n+        \"\"\"Commit the blob.\n+        @return: The committed blob\n+        @rtype: L{Blob}\"\"\"\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+    \"\"\"Represents a git blob object. All the actual data contents of the\n+    blob object is stored in the L{data} member, which is a\n+    L{BlobData} object.\"\"\"\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+    \"\"\"A dictionary that cannot be modified once it's been created.\"\"\"\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(Immutable, Repr):\n+    \"\"\"Represents the data contents of a git tree object.\"\"\"\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+        \"\"\"Create a new L{TreeData} object from the given mapping from names\n+        (strings) to either (I{permission}, I{object}) tuples or just\n+        objects.\"\"\"\n+        self.__entries = ImmutableDict((name, self.__x(po))\n+                                       for (name, po) in entries.iteritems())\n+    entries = property(lambda self: self.__entries)\n+    \"\"\"Map from name to (I{permission}, I{object}) tuple.\"\"\"\n+    def set_entry(self, name, po):\n+        \"\"\"Create a new L{TreeData} object identical to this one, except that\n+        it maps C{name} to C{po}.\n+\n+        @param name: Name of the changed mapping\n+        @type name: C{str}\n+        @param po: Value of the changed mapping\n+        @type po: L{Blob} or L{Tree} or (C{str}, L{Blob} or L{Tree})\n+        @return: The new L{TreeData} object\n+        @rtype: L{TreeData}\"\"\"\n+        e = dict(self.entries)\n+        e[name] = self.__x(po)\n+        return type(self)(e)\n+    def del_entry(self, name):\n+        \"\"\"Create a new L{TreeData} object identical to this one, except that\n+        it doesn't map C{name} to anything.\n+\n+        @param name: Name of the deleted mapping\n+        @type name: C{str}\n+        @return: The new L{TreeData} object\n+        @rtype: L{TreeData}\"\"\"\n+        e = dict(self.entries)\n+        del e[name]\n+        return type(self)(e)\n+    def commit(self, repository):\n+        \"\"\"Commit the tree.\n+        @return: The committed tree\n+        @rtype: L{Tree}\"\"\"\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+        \"\"\"Parse a raw git tree description.\n+\n+        @return: A new L{TreeData} object\n+        @rtype: L{TreeData}\"\"\"\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+    \"\"\"Represents a git tree object. All the actual data contents of the\n+    tree object is stored in the L{data} member, which is a\n+    L{TreeData} object.\"\"\"\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(Immutable, Repr):\n-    \"\"\"Represents the actual data contents of a git commit object.\"\"\"\n+    \"\"\"Represents the data contents of a git commit object.\"\"\"\n     def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,\n                  committer = NoValue, message = NoValue, defaults = NoValue):\n         d = make_defaults(defaults)\n@@ -238,8 +364,30 @@ class CommitData(Immutable, 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+        \"\"\"Commit the commit.\n+        @return: The committed commit\n+        @rtype: L{Commit}\"\"\"\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+        \"\"\"Parse a raw git commit description.\n+        @return: A new L{CommitData} object\n+        @rtype: L{CommitData}\"\"\"\n         cd = cls(parents = [])\n         lines = list(s.splitlines(True))\n         for i in xrange(len(lines)):\n@@ -259,10 +407,11 @@ class CommitData(Immutable, Repr):\n                 assert False\n         assert False\n \n-class Commit(Immutable, Repr):\n+class Commit(GitObject):\n     \"\"\"Represents a git commit object. All the actual data contents of the\n     commit object is stored in the L{data} member, which is a\n     L{CommitData} object.\"\"\"\n+    typename = 'commit'\n     def __init__(self, repository, sha1):\n         self.__sha1 = sha1\n         self.__repository = repository\n@@ -367,7 +516,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@@ -429,26 +579,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_ref(self):\n         try:\n"},{"id":"79546","messageId":"20080612053424.23549.64457.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:25Z","receivedAt":"2008-06-12T05:34:25Z","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\nThe only thing you can do with this log at the moment is look at it.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/branch.py  |   19 ++-\n stgit/commands/common.py  |    8 +\n stgit/commands/diff.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/show.py    |    2 \n stgit/commands/status.py  |    3 -\n stgit/lib/git.py          |    3 -\n stgit/lib/log.py          |  254 +++++++++++++++++++++++++++++++++++++++++++++\n stgit/lib/stack.py        |    9 ++\n stgit/lib/transaction.py  |    3 -\n stgit/main.py             |    2 \n 15 files changed, 298 insertions(+), 17 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..edbb01c 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('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+                     '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,7 @@ 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], 'branch --rename')\n         return\n \n     elif options.unprotect:\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex 029ec65..fd02398 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/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/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 a5b2f88..4d13112 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/lib/git.py b/stgit/lib/git.py\nindex a8881f4..35e03d2 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -139,6 +139,7 @@ class Person(Immutable, Repr):\n         assert isinstance(self.__date, Date) or self.__date in [None, NoValue]\n     name = property(lambda self: self.__name)\n     email = property(lambda self: self.__email)\n+    name_email = property(lambda self: '%s <%s>' % (self.name, self.email))\n     date = property(lambda self: self.__date)\n     def set_name(self, name):\n         return type(self)(name = name, defaults = self)\n@@ -147,7 +148,7 @@ class Person(Immutable, Repr):\n     def set_date(self, date):\n         return type(self)(date = date, defaults = self)\n     def __str__(self):\n-        return '%s <%s> %s' % (self.name, self.email, self.date)\n+        return '%s %s' % (self.name_email, self.date)\n     @classmethod\n     def parse(cls, s):\n         m = re.match(r'^([^<]*)<([^>]*)>\\s+(\\d+\\s+[+-]\\d{4})$', s)\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nnew file mode 100644\nindex 0000000..8646e08\n--- /dev/null\n+++ b/stgit/lib/log.py\n@@ -0,0 +1,254 @@\n+r\"\"\"This module contains functions and classes for manipulating\n+I{patch stack logs} (or just I{stack logs}).\n+\n+A stack log is a git branch. Each commit contains the complete state\n+of the stack at the moment it was written; the most recent commit has\n+the most recent state.\n+\n+For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.\n+\n+Stack log format (version 4)\n+============================\n+\n+I{This describes version 4 of the stack log format. Versions 0 throgh\n+3 were earlier development versions that are no longer supported.}\n+\n+Commit message\n+--------------\n+\n+The commit message is meant for human consumption; in most cases it is\n+just a subject line: the stg subcommand name and possibly some\n+important command-line flag.\n+\n+The exception is log commits for undo and redo. Their subject line is\n+\"C{undo I{n}}\" and \"C{redo I{n}}\"; the positive integer I{n} says how\n+many steps were undone or redone.\n+\n+Tree\n+----\n+\n+The top-level tree object has the following entries:\n+\n+  - C{version}: a blob containing the string \"C{5\\n}\".\n+\n+  - C{head}: a blob containing the ASCII hex sha1 of the current HEAD,\n+    followed by a newline.\n+\n+  - C{applied}, C{unapplied}: blobs containing one line for each\n+    applied or unapplied patch, in order. Each line consists of\n+\n+      - the ASCII hex sha1 of the patch's commit object,\n+\n+      - a space, and\n+\n+      - the patch's name.\n+\n+  - C{patches}: a tree containing one subtree for each patch, named\n+    after that patch. Each such subtree contains:\n+\n+      - C{a}, C{b}: the patch's I{bottom} and I{top} trees.\n+\n+      - C{info}: a blob containing::\n+\n+          Author: <author name and e-mail>\n+          Date: <patch timestamp>\n+\n+          <commit message>\n+\n+Parents\n+-------\n+\n+  - The first parent is the I{simplified log}, described below.\n+\n+  - The second and third parents are the branch head and the stack\n+    top, in any order; if they are identical (which is always the case\n+    unless StGit's metadata is messed up by use of a non-StGit tool),\n+    just one of them is listed.\n+\n+  - The last parent is the previous log entry. (If there is no\n+    previous log entry, it is omitted.) This log entry must be version\n+    E{4}.\n+\n+Simplified log\n+--------------\n+\n+The simplified log looks exactly like the normal, or I{full}, log,\n+except for the following:\n+\n+  - Instead of having a tree per patch in the C{patches} subtree, it\n+    has a blob per patch. This blob contains::\n+\n+      Bottom: <sha1 of patch's bottom tree>\n+      Top:    <sha1 of patch's top tree>\n+      Author: <author name and e-mail>\n+      Date:   <patch timestamp>\n+\n+      <commit message>\n+\n+      ---\n+\n+      <patch's diff>\n+\n+  - Its only parent is the simplified version of the previous log\n+    entry.\n+\n+The simplified log contains no information not in the full log; its\n+purpose is ease of visualization.\"\"\"\n+\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 = 4\n+\n+def commit_info(cd):\n+    return 'Author: %s\\nDate: %s\\n\\n%s' % (cd.author.name_email,\n+                                           cd.author.date, 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 patch_file(repository, cd):\n+    return repository.commit(git.BlobData(''.join(s + '\\n' for s in [\n+                    'Bottom: %s' % cd.parent.data.tree.sha1,\n+                    'Top:    %s' % cd.tree.sha1,\n+                    'Author: %s' % cd.author.name_email,\n+                    'Date:   %s' % cd.author.date,\n+                    '',\n+                    cd.message,\n+                    '',\n+                    '---',\n+                    '',\n+                    repository.diff_tree(cd.parent.data.tree, cd.tree, ['-M']\n+                                         ).strip()])))\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_trees(repository, stack):\n+    full_patches, short_patches = [repository.commit(git.TreeData(dict(\n+                    (pn, x(repository, stack.patches.get(pn).commit.data))\n+                    for pn in stack.patchorder.all)))\n+                                   for x in [patch_tree, patch_file]]\n+    version = repository.commit(git.BlobData('%d\\n' % _current_version))\n+    head = repository.commit(git.BlobData('%s\\n' % stack.head.sha1))\n+    applied = order_blob(repository, stack, 'applied')\n+    unapplied = order_blob(repository, stack, 'unapplied')\n+    return [repository.commit(git.TreeData({\n+                    'version': version,\n+                    'head': head,\n+                    'patches': patches,\n+                    'applied': applied,\n+                    'unapplied': unapplied,\n+                    })) for patches in [full_patches, short_patches]]\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+    full_log_tree, short_log_tree = log_entry_trees(stack.repository, stack)\n+    stack_log = stack.repository.commit(\n+        git.CommitData(tree = short_log_tree, message = msg,\n+                       parents = [ll.stack_log for ll in last_log]))\n+    refs = list(set([stack.head, stack.top]))\n+    full_log = stack.repository.commit(\n+        git.CommitData(tree = full_log_tree, message = msg,\n+                       parents = ([stack_log] + refs\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+    \"\"\"Write a new log entry. (Convenience function intended for use by\n+    code not yet converted to the new infrastructure.)\"\"\"\n+    repo = default_repo()\n+    stack = repo.get_stack(repo.current_branch_name)\n+    log_entry(stack, msg)\n+\n+class Log(object):\n+    \"\"\"Class used to read the stack log. Each instance represents one log\n+    entry.\"\"\"\n+    def __init__(self, repo, ref, commit):\n+        \"\"\"Create a L{Log} object representing the log entry C{commit}. In any\n+        error messages, say C{ref} when we mean C{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.strip())\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+    \"\"\"Commit of the full log.\"\"\"\n+    stack_log = property(lambda self: self.commit.data.parents[0])\n+    \"\"\"Commit of the simplified log.\"\"\"\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/stack.py b/stgit/lib/stack.py\nindex 9cb3967..62a1ec2 100644\n--- a/stgit/lib/stack.py\n+++ b/stgit/lib/stack.py\n@@ -165,6 +165,15 @@ class Stack(git.Branch):\n                                     ).commit.data.parent\n         else:\n             return self.head\n+    @property\n+    def top(self):\n+        \"\"\"Commit of the topmost patch, or the stack base if no patches are\n+        applied.\"\"\"\n+        if self.patchorder.applied:\n+            return self.patches.get(self.patchorder.applied[-1]).commit\n+        else:\n+            # When no patches are applied, base == head.\n+            return self.head\n     def head_top_equal(self):\n         if not self.patchorder.applied:\n             return True\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex e47997e..4c4da1a 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -4,7 +4,7 @@ updates to an StGit stack in a safe and convenient way.\"\"\"\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     \"\"\"Exception raised when something goes wrong with a\n@@ -170,6 +170,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..ec0e840 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(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(cmd)\n     sys.exit(ret or utils.STGIT_SUCCESS)\n"},{"id":"79545","messageId":"20080612053432.23549.67083.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 04/14] Add utility function for reordering patches","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:32Z","receivedAt":"2008-06-12T05:34:32Z","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 4c4da1a..16f5a4b 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -1,6 +1,8 @@\n \"\"\"The L{StackTransaction} class makes it possible to make complex\n updates to an StGit stack in a safe and convenient way.\"\"\"\n \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@@ -274,3 +276,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":"79548","messageId":"20080612053438.23549.85715.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 05/14] New command: stg reset","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:38Z","receivedAt":"2008-06-12T05:34:38Z","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 |  116 ++++++++++++++++++++++++++++++++++++\n stgit/main.py           |    2 +\n t/t3100-reset.sh        |  151 +++++++++++++++++++++++++++++++++++++++++++++++\n 3 files changed, 269 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..b2643b1\n--- /dev/null\n+++ b/stgit/commands/reset.py\n@@ -0,0 +1,116 @@\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 (or rather,\n+the last N log entries; there is not an exact one-to-one\n+relationship), 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, '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 ec0e840..83e6b08 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":"79549","messageId":"20080612053444.23549.59793.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 06/14] Log conflicts separately","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:44Z","receivedAt":"2008-06-12T05:34:44Z","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 16f5a4b..6347b14 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -79,6 +79,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@@ -160,19 +161,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@@ -264,18 +272,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":"79550","messageId":"20080612053450.23549.69382.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 07/14] Log conflicts separately for all commands","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:50Z","receivedAt":"2008-06-12T05:34:50Z","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 commands behave consistently.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/lib/log.py   |   32 +++++++++++++++++++++++++++++++-\n stgit/lib/stack.py |    7 +++++++\n 2 files changed, 38 insertions(+), 1 deletions(-)\n\n\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex 8646e08..920c261 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -174,12 +174,42 @@ 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 L{Stack<stgit.lib.stack.Stack>}, but with the\n+    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.top = stack.top.data.parent\n+        self.base = stack.base\n+        self.name = stack.name\n+        self.repository = stack.repository\n def compat_log_entry(msg):\n     \"\"\"Write a new log entry. (Convenience function intended for use by\n     code not yet converted to the new infrastructure.)\"\"\"\n     repo = default_repo()\n     stack = repo.get_stack(repo.current_branch_name)\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     \"\"\"Class used to read the stack log. Each instance represents one log\ndiff --git a/stgit/lib/stack.py b/stgit/lib/stack.py\nindex 62a1ec2..2d686e6 100644\n--- a/stgit/lib/stack.py\n+++ b/stgit/lib/stack.py\n@@ -25,6 +25,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":"79554","messageId":"20080612053456.23549.41821.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 08/14] Add a --hard flag to stg reset","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:34:56Z","receivedAt":"2008-06-12T05:34:56Z","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 has polluted the index+worktree with a heap\nof conflicts.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/reset.py  |   13 +++++++----\n stgit/lib/git.py         |    4 +++\n stgit/lib/transaction.py |   17 ++++++++++++--\n t/t3101-reset-hard.sh    |   56 ++++++++++++++++++++++++++++++++++++++++++++++\n 4 files changed, 82 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 b2643b1..a7b5d35 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@@ -43,9 +44,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@@ -55,7 +57,7 @@ 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, '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@@ -113,4 +115,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 35e03d2..4c2605b 100644\n--- a/stgit/lib/git.py\n+++ b/stgit/lib/git.py\n@@ -736,6 +736,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 6347b14..10c9b39 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -73,7 +73,14 @@ class StackTransaction(object):\n       method. This will either succeed in writing the updated state to\n       your refs and index+worktree, or fail without having done\n       anything.\"\"\"\n-    def __init__(self, stack, msg, allow_conflicts = False):\n+    def __init__(self, stack, msg, discard_changes = False,\n+                 allow_conflicts = False):\n+        \"\"\"Create a new L{StackTransaction}.\n+\n+        @param discard_changes: Discard any changes in index+worktree\n+        @type discard_changes: bool\n+        @param allow_conflicts: Whether to allow pre-existing conflicts\n+        @type allow_conflicts: bool or function of L{StackTransaction}\"\"\"\n         self.__stack = stack\n         self.__msg = msg\n         self.__patches = _TransPatchMap(stack)\n@@ -83,6 +90,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@@ -107,7 +115,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@@ -118,7 +126,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":"79553","messageId":"20080612053502.23549.64988.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 09/14] Don't write a log entry if there were no changes","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:02Z","receivedAt":"2008-06-12T05:35:02Z","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 920c261..3aec6e7 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -164,6 +164,9 @@ def log_entry(stack, msg):\n         out.warn(str(e), 'No log entry written.')\n         return\n     full_log_tree, short_log_tree = log_entry_trees(stack.repository, stack)\n+    if len(last_log) == 1 and full_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 = short_log_tree, message = msg,\n                        parents = [ll.stack_log for ll in last_log]))\n"},{"id":"79555","messageId":"20080612053508.23549.98668.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 10/14] Move stack reset function to a shared location","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:08Z","receivedAt":"2008-06-12T05:35:08Z","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         |   62 +++++++++++++++++++++++++++++++++++++++++\n stgit/lib/transaction.py |    3 +-\n 3 files changed, 71 insertions(+), 64 deletions(-)\n\n\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nindex a7b5d35..5ad9914 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/reset.py\n@@ -47,67 +47,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@@ -115,5 +54,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 3aec6e7..2449913 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -285,3 +285,65 @@ 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+    \"\"\"Reset the stack to a given previous state. If C{only_patches} is\n+    not empty, touch only patches whose names appear in it.\n+\n+    @param only_patches: Reset only these patches\n+    @type only_patches: iterable\"\"\"\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 10c9b39..2003105 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -103,6 +103,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@@ -136,7 +137,7 @@ class StackTransaction(object):\n         raise TransactionException(\n             'Command aborted (all changes rolled back)')\n     def __check_consistency(self):\n-        remaining = set(self.__applied + self.__unapplied)\n+        remaining = set(self.all_patches)\n         for pn, commit in self.__patches.iteritems():\n             if commit == None:\n                 assert self.__stack.patches.exists(pn)\n"},{"id":"79556","messageId":"20080612053513.23549.70063.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 11/14] New command: stg undo","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:13Z","receivedAt":"2008-06-12T05:35:13Z","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  |   40 +++++++---------------\n stgit/lib/log.py        |   38 +++++++++++++++++++++\n stgit/main.py           |    2 +\n t/t3102-undo.sh         |   86 +++++++++++++++++++++++++++++++++++++++++++++++\n t/t3103-undo-hard.sh    |   10 +++--\n 5 files changed, 144 insertions(+), 32 deletions(-)\n copy stgit/commands/{reset.py => undo.py} (56%)\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 56%\ncopy from stgit/commands/reset.py\ncopy to stgit/commands/undo.py\nindex 5ad9914..b1d7de9 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/undo.py\n@@ -22,42 +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 (or rather,\n-the last N log entries; there is not an exact one-to-one\n-relationship), 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 2449913..3b242cd 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -95,6 +95,7 @@ except for the following:\n The simplified log contains no information not in the full log; its\n purpose is ease of visualization.\"\"\"\n \n+import re\n from stgit.lib import git, stack\n from stgit import exception\n from stgit.out import out\n@@ -258,6 +259,16 @@ class Log(object):\n             self.base = self.patches[self.applied[0]].data.parent\n         else:\n             self.base = self.head\n+    @property\n+    def top(self):\n+        if self.applied:\n+            return self.patches[self.applied[-1]]\n+        else:\n+            return self.head\n+    @property\n+    def parents(self):\n+        num_refs = len(set([self.top, self.head]))\n+        return self.commit.data.parents[(1 + num_refs):]\n \n class FullLog(Log):\n     full_log = property(lambda self: self.commit)\n@@ -347,3 +358,30 @@ 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+    \"\"\"Find the log entry C{undo_steps} steps in the past. (Successive\n+    undo operations are supposed to \"add up\", so if we find other undo\n+    operations along the way we have to add those undo steps to\n+    C{undo_steps}.)\n+\n+    @return: The log entry that is the destination of the undo\n+             operation\n+    @rtype: L{Log}\"\"\"\n+    ref = log_ref(stack.name)\n+    try:\n+        commit = stack.repository.refs.get(ref)\n+    except KeyError:\n+        raise LogException('Log is empty')\n+    log = Log(stack.repository, ref, commit)\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"},{"id":"79551","messageId":"20080612053519.23549.98485.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 12/14] New command: stg redo","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:19Z","receivedAt":"2008-06-12T05:35:19Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Command for undoing an undo.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/redo.py |   21 +++++++++------\n stgit/lib/log.py       |   21 ++++++++++++---\n stgit/main.py          |    2 +\n t/t3104-redo.sh        |   66 +++++++++++++++++++++++++++++++++++++-----------\n 4 files changed, 81 insertions(+), 29 deletions(-)\n copy stgit/commands/{undo.py => redo.py} (71%)\n copy t/{t3102-undo.sh => t3104-redo.sh} (53%)\n\n\ndiff --git a/stgit/commands/undo.py b/stgit/commands/redo.py\nsimilarity index 71%\ncopy from stgit/commands/undo.py\ncopy to stgit/commands/redo.py\nindex b1d7de9..47221a5 100644\n--- a/stgit/commands/undo.py\n+++ b/stgit/commands/redo.py\n@@ -19,28 +19,31 @@ Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA\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+from stgit.lib import log, transaction\n \n-help = 'undo the last operation'\n+help = 'undo the last undo operation'\n usage = \"\"\"%prog [options]\n \n-Reset the patch stack to the previous state. Consecutive invocations\n-of \"stg undo\" will take you ever further into the past.\"\"\"\n+If the last command was an undo, reset the patch stack to the state it\n+had before the undo. Consecutive invocations of \"stg redo\" will undo\n+the effects of consecutive invocations of \"stg undo\".\n+\n+It is an error to run \"stg redo\" if the last command was not an\n+undo.\"\"\"\n \n directory = common.DirectoryHasRepositoryLib()\n options = [make_option('-n', '--number', type = 'int', metavar = 'N',\n                        default = 1,\n-                       help = 'undo the last N commands'),\n+                       help = 'undo the last N undos'),\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 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+        raise common.CmdException('Bad number of undos to redo')\n+    state = log.undo_state(stack, -options.number)\n+    trans = transaction.StackTransaction(stack, 'redo %d' % options.number,\n                                          discard_changes = options.hard)\n     try:\n         log.reset_stack(trans, stack.repository.default_iw, state, [])\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex 3b242cd..a8de06b 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -365,6 +365,8 @@ def undo_state(stack, undo_steps):\n     operations along the way we have to add those undo steps to\n     C{undo_steps}.)\n \n+    If C{undo_steps} is negative, redo instead of undo.\n+\n     @return: The log entry that is the destination of the undo\n              operation\n     @rtype: L{Log}\"\"\"\n@@ -374,13 +376,22 @@ def undo_state(stack, undo_steps):\n     except KeyError:\n         raise LogException('Log is empty')\n     log = Log(stack.repository, ref, commit)\n-    while undo_steps > 0:\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+        um = re.match(r'^undo\\s+(\\d+)$', msg)\n+        if undo_steps > 0:\n+            if um:\n+                undo_steps += int(um.group(1))\n+            else:\n+                undo_steps -= 1\n         else:\n-            undo_steps -= 1\n+            rm = re.match(r'^redo\\s+(\\d+)$', msg)\n+            if um:\n+                undo_steps += 1\n+            elif rm:\n+                undo_steps -= int(rm.group(1))\n+            else:\n+                raise LogException('No more redo information available')\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])\ndiff --git a/stgit/main.py b/stgit/main.py\nindex cf7b404..c53abf7 100644\n--- a/stgit/main.py\n+++ b/stgit/main.py\n@@ -86,6 +86,7 @@ commands = Commands({\n     'pull':             'pull',\n     'push':             'push',\n     'rebase':           'rebase',\n+    'redo':             'redo',\n     'refresh':          'refresh',\n     'rename':           'rename',\n     'repair':           'repair',\n@@ -123,6 +124,7 @@ stackcommands = (\n     'pull',\n     'push',\n     'rebase',\n+    'redo',\n     'repair',\n     'reset',\n     'series',\ndiff --git a/t/t3102-undo.sh b/t/t3104-redo.sh\nsimilarity index 53%\ncopy from t/t3102-undo.sh\ncopy to t/t3104-redo.sh\nindex 1093f70..290fc6f 100755\n--- a/t/t3102-undo.sh\n+++ b/t/t3104-redo.sh\n@@ -1,6 +1,6 @@\n #!/bin/sh\n \n-test_description='Simple test cases for \"stg undo\"'\n+test_description='Simple test cases for \"stg redo\"'\n \n . ./test-lib.sh\n \n@@ -20,18 +20,18 @@ test_expect_success 'Initialize StGit stack with three patches' '\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+    stg uncommit -n 3\n '\n \n cat > expected.txt <<EOF\n 000\n 111\n+222\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 \"$(echo $(stg applied))\" = \"p1 p2\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n     test_cmp expected.txt a\n '\n \n@@ -39,9 +39,22 @@ cat > expected.txt <<EOF\n 000\n 111\n 222\n+333\n EOF\n-test_expect_success '... and undo it' '\n+test_expect_success '... undo it ...' '\n     stg undo &&\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 redo' '\n+    stg redo &&\n     test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n     test \"$(echo $(stg unapplied))\" = \"p3\" &&\n     test_cmp expected.txt a\n@@ -50,7 +63,9 @@ test_expect_success '... and undo it' '\n cat > expected.txt <<EOF\n 000\n EOF\n-test_expect_success 'Pop two patches ...' '\n+test_expect_success 'Pop three patches ...' '\n+    stg push &&\n+    stg pop &&\n     stg pop &&\n     stg pop &&\n     test \"$(echo $(stg applied))\" = \"\" &&\n@@ -62,24 +77,45 @@ cat > expected.txt <<EOF\n 000\n 111\n 222\n+333\n EOF\n-test_expect_success '... and undo it' '\n+test_expect_success '... undo it ...' '\n     stg undo &&\n     stg undo &&\n-    test \"$(echo $(stg applied))\" = \"p1 p2\" &&\n-    test \"$(echo $(stg unapplied))\" = \"p3\" &&\n+    stg undo &&\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 '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_expect_success '... redo the first two pops ...' '\n+    stg redo -n 2 &&\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+EOF\n+test_expect_success '... and the remaining one' '\n+    stg redo &&\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+EOF\n+test_expect_success 'Redo past end of history' '\n+    ! stg redo &&\n+    test \"$(echo $(stg applied))\" = \"\" &&\n+    test \"$(echo $(stg unapplied))\" = \"p1 p2 p3\" &&\n     test_cmp expected.txt a\n '\n \n"},{"id":"79552","messageId":"20080612053525.23549.49690.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 13/14] Log and undo external modifications","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:25Z","receivedAt":"2008-06-12T05:35:25Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"At the beginning of every StGit command, quickly check if the branch\nhead recorded in the log is the same as the actual branch head; if\nit's not, conclude that some non-StGit tool has modified the stack,\nand record a log entry that says so. (Additionally, if the log doesn't\nexist yet, create it.)\n\nThis introduces the possibility that a log entry specifies a head and\na top that aren't equal. So teach undo, redo, and reset to deal with\nthat case.\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/common.py     |    1 \n stgit/commands/redo.py       |    4 +-\n stgit/commands/reset.py      |    8 +++-\n stgit/commands/undo.py       |    4 +-\n stgit/lib/log.py             |   88 ++++++++++++++++++++++++++++--------------\n stgit/lib/transaction.py     |   39 ++++++++++++-------\n t/t3105-undo-external-mod.sh |   68 ++++++++++++++++++++++++++++++++\n 7 files changed, 162 insertions(+), 50 deletions(-)\n create mode 100755 t/t3105-undo-external-mod.sh\n\n\ndiff --git a/stgit/commands/common.py b/stgit/commands/common.py\nindex fd02398..2689a42 100644\n--- a/stgit/commands/common.py\n+++ b/stgit/commands/common.py\n@@ -525,6 +525,7 @@ class DirectoryAnywhere(_Directory):\n class DirectoryHasRepository(_Directory):\n     def setup(self):\n         self.git_dir # might throw an exception\n+        log.compat_log_external_mods()\n \n class DirectoryInWorktree(DirectoryHasRepository):\n     def setup(self):\ndiff --git a/stgit/commands/redo.py b/stgit/commands/redo.py\nindex 47221a5..a1075ec 100644\n--- a/stgit/commands/redo.py\n+++ b/stgit/commands/redo.py\n@@ -46,7 +46,7 @@ def func(parser, options, args):\n     trans = transaction.StackTransaction(stack, 'redo %d' % options.number,\n                                          discard_changes = options.hard)\n     try:\n-        log.reset_stack(trans, stack.repository.default_iw, state, [])\n+        log.reset_stack(trans, stack.repository.default_iw, state)\n     except transaction.TransactionHalted:\n         pass\n-    return trans.run(stack.repository.default_iw)\n+    return trans.run(stack.repository.default_iw, allow_bad_head = True)\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nindex 5ad9914..22d7731 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/reset.py\n@@ -57,7 +57,11 @@ def func(parser, options, args):\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+        if patches:\n+            log.reset_stack_partially(trans, stack.repository.default_iw,\n+                                      state, patches)\n+        else:\n+            log.reset_stack(trans, stack.repository.default_iw, state)\n     except transaction.TransactionHalted:\n         pass\n-    return trans.run(stack.repository.default_iw)\n+    return trans.run(stack.repository.default_iw, allow_bad_head = not patches)\ndiff --git a/stgit/commands/undo.py b/stgit/commands/undo.py\nindex b1d7de9..58f1da6 100644\n--- a/stgit/commands/undo.py\n+++ b/stgit/commands/undo.py\n@@ -43,7 +43,7 @@ def func(parser, options, args):\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, [])\n+        log.reset_stack(trans, stack.repository.default_iw, state)\n     except transaction.TransactionHalted:\n         pass\n-    return trans.run(stack.repository.default_iw)\n+    return trans.run(stack.repository.default_iw, allow_bad_head = True)\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex a8de06b..8c0516b 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -259,6 +259,7 @@ class Log(object):\n             self.base = self.patches[self.applied[0]].data.parent\n         else:\n             self.base = self.head\n+    all_patches = property(lambda self: self.applied + self.unapplied)\n     @property\n     def top(self):\n         if self.applied:\n@@ -297,34 +298,33 @@ def copy_log(repo, src_branch, dst_branch, msg):\n def default_repo():\n     return stack.Repository.default()\n \n-def reset_stack(trans, iw, state, only_patches):\n-    \"\"\"Reset the stack to a given previous state. If C{only_patches} is\n-    not empty, touch only patches whose names appear in it.\n+def reset_stack(trans, iw, state):\n+    \"\"\"Reset the stack to a given previous state.\"\"\"\n+    for pn in trans.all_patches:\n+        trans.patches[pn] = None\n+    for pn in state.all_patches:\n+        trans.patches[pn] = state.patches[pn]\n+    trans.applied = state.applied\n+    trans.unapplied = state.unapplied\n+    trans.base = state.base\n+    trans.head = state.head\n+\n+def reset_stack_partially(trans, iw, state, only_patches):\n+    \"\"\"Reset the stack to a given previous state -- but only the given\n+    patches, not anything else.\n \n-    @param only_patches: Reset only these patches\n+    @param only_patches: Touch only these patches\n     @type only_patches: iterable\"\"\"\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+    patches_to_reset = set(state.applied + state.unapplied) & only_patches\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+    to_delete = (existing_patches - patches_to_reset) & only_patches\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+        if not pn in only_patches:\n             return False\n         if pn in to_delete:\n             return True\n@@ -347,17 +347,12 @@ def reset_stack(trans, iw, state, only_patches):\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)\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 \n def undo_state(stack, undo_steps):\n     \"\"\"Find the log entry C{undo_steps} steps in the past. (Successive\n@@ -396,3 +391,36 @@ def undo_state(stack, undo_steps):\n             raise LogException('Not enough undo information available')\n         log = Log(stack.repository, log.parents[0].sha1, log.parents[0])\n     return log\n+\n+def log_external_mods(stack):\n+    ref = log_ref(stack.name)\n+    try:\n+        log_commit = stack.repository.refs.get(ref)\n+    except KeyError:\n+        # No log exists yet.\n+        log_entry(stack, 'start of log')\n+        return\n+    try:\n+        log = Log(stack.repository, ref, log_commit)\n+    except LogException:\n+        # Something's wrong with the log, so don't bother.\n+        return\n+    if log.head == stack.head:\n+        # No external modifications.\n+        return\n+    log_entry(stack, '\\n'.join([\n+                'external modifications', '',\n+                'Modifications by tools other than StGit (e.g. git).']))\n+\n+def compat_log_external_mods():\n+    try:\n+        repo = default_repo()\n+    except git.RepositoryException:\n+        # No repository, so we couldn't log even if we wanted to.\n+        return\n+    try:\n+        stack = repo.get_stack(repo.current_branch_name)\n+    except exception.StgException:\n+        # Stack doesn't exist, so we can't log.\n+        return\n+    log_external_mods(stack)\ndiff --git a/stgit/lib/transaction.py b/stgit/lib/transaction.py\nindex 2003105..0c8d9a5 100644\n--- a/stgit/lib/transaction.py\n+++ b/stgit/lib/transaction.py\n@@ -91,6 +91,7 @@ class StackTransaction(object):\n         self.__current_tree = self.__stack.head.data.tree\n         self.__base = self.__stack.base\n         self.__discard_changes = discard_changes\n+        self.__bad_head = None\n         if isinstance(allow_conflicts, bool):\n             self.__allow_conflicts = lambda trans: allow_conflicts\n         else:\n@@ -109,8 +110,22 @@ class StackTransaction(object):\n                 or self.patches[self.applied[0]].data.parent == val)\n         self.__base = val\n     base = property(lambda self: self.__base, __set_base)\n-    def __checkout(self, tree, iw):\n-        if not self.__stack.head_top_equal():\n+    @property\n+    def top(self):\n+        if self.__applied:\n+            return self.__patches[self.__applied[-1]]\n+        else:\n+            return self.__base\n+    def __get_head(self):\n+        if self.__bad_head:\n+            return self.__bad_head\n+        else:\n+            return self.top\n+    def __set_head(self, val):\n+        self.__bad_head = val\n+    head = property(__get_head, __set_head)\n+    def __checkout(self, tree, iw, allow_bad_head):\n+        if not (allow_bad_head or self.__stack.head_top_equal()):\n             out.error(\n                 'HEAD and top are not the same.',\n                 'This can happen if you modify a branch with git.',\n@@ -143,26 +158,22 @@ class StackTransaction(object):\n                 assert self.__stack.patches.exists(pn)\n             else:\n                 assert pn in remaining\n-    @property\n-    def __head(self):\n-        if self.__applied:\n-            return self.__patches[self.__applied[-1]]\n-        else:\n-            return self.__base\n     def abort(self, iw = None):\n         # The only state we need to restore is index+worktree.\n         if iw:\n-            self.__checkout(self.__stack.head.data.tree, iw)\n-    def run(self, iw = None):\n+            self.__checkout(self.__stack.head.data.tree, iw,\n+                            allow_bad_head = True)\n+    def run(self, iw = None, allow_bad_head = False):\n         \"\"\"Execute the transaction. Will either succeed, or fail (with an\n         exception) and do nothing.\"\"\"\n         self.__check_consistency()\n-        new_head = self.__head\n+        log.log_external_mods(self.__stack)\n+        new_head = self.head\n \n         # Set branch head.\n         if iw:\n             try:\n-                self.__checkout(new_head.data.tree, iw)\n+                self.__checkout(new_head.data.tree, iw, allow_bad_head)\n             except git.CheckoutException:\n                 # We have to abort the transaction.\n                 self.abort(iw)\n@@ -257,7 +268,7 @@ class StackTransaction(object):\n         cd = orig_cd.set_committer(None)\n         s = ['', ' (empty)'][cd.is_nochange()]\n         oldparent = cd.parent\n-        cd = cd.set_parent(self.__head)\n+        cd = cd.set_parent(self.top)\n         base = oldparent.data.tree\n         ours = cd.parent.data.tree\n         theirs = cd.tree\n@@ -267,7 +278,7 @@ class StackTransaction(object):\n             if iw == None:\n                 self.__halt('%s does not apply cleanly' % pn)\n             try:\n-                self.__checkout(ours, iw)\n+                self.__checkout(ours, iw, allow_bad_head = False)\n             except git.CheckoutException:\n                 self.__halt('Index/worktree dirty')\n             try:\ndiff --git a/t/t3105-undo-external-mod.sh b/t/t3105-undo-external-mod.sh\nnew file mode 100755\nindex 0000000..289a42a\n--- /dev/null\n+++ b/t/t3105-undo-external-mod.sh\n@@ -0,0 +1,68 @@\n+#!/bin/sh\n+\n+test_description='Undo external modifications of the stack'\n+\n+. ./test-lib.sh\n+\n+# Ignore our own output files.\n+cat > .git/info/exclude <<EOF\n+/expected.txt\n+/head?.txt\n+EOF\n+\n+test_expect_success 'Initialize StGit stack' '\n+    stg init &&\n+    echo 000 >> a &&\n+    git add a &&\n+    git commit -m p0 &&\n+    echo 111 >> a &&\n+    git add a &&\n+    git commit -m p1 &&\n+    stg uncommit -n 1\n+'\n+\n+cat > expected.txt <<EOF\n+000\n+111\n+222\n+EOF\n+test_expect_success 'Make a git commit and turn it into a patch' '\n+    git rev-parse HEAD > head0.txt &&\n+    echo 222 >> a &&\n+    git add a &&\n+    git commit -m p2 &&\n+    git rev-parse HEAD > head1.txt &&\n+    stg repair &&\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 'Undo the patchification' '\n+    stg undo &&\n+    git rev-parse HEAD > head2.txt &&\n+    test_cmp head1.txt head2.txt &&\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+EOF\n+test_expect_success 'Undo the commit' '\n+    stg undo &&\n+    git rev-parse HEAD > head3.txt &&\n+    test_cmp head0.txt head3.txt &&\n+    test \"$(echo $(stg applied))\" = \"p1\" &&\n+    test \"$(echo $(stg unapplied))\" = \"\" &&\n+    test_cmp expected.txt a\n+'\n+\n+test_done\n"},{"id":"79557","messageId":"20080612053531.23549.77890.stgit@yoghurt","threadId":"13908","inReplyTo":"20080612052913.23549.69687.stgit@yoghurt","subject":"[StGit PATCH 14/14] Make \"stg log\" show stack log instead of patch log","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-12T05:35:31Z","receivedAt":"2008-06-12T05:35:31Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"Make \"stg log\" show the new stack log instead of the old patch logs,\nwhich is now obsolete. Delete t1400-patch-history, which is specific\nto the old \"stg log\".\n\nSigned-off-by: Karl Hasselström <kha@treskal.com>\n\n---\n\n stgit/commands/log.py    |  169 ++++++++++++++--------------------------------\n stgit/commands/reset.py  |   15 +---\n stgit/lib/log.py         |    3 +\n t/t1400-patch-history.sh |  103 ----------------------------\n 4 files changed, 58 insertions(+), 232 deletions(-)\n delete mode 100755 t/t1400-patch-history.sh\n\n\ndiff --git a/stgit/commands/log.py b/stgit/commands/log.py\nindex 13e0baa..cf15c7d 100644\n--- a/stgit/commands/log.py\n+++ b/stgit/commands/log.py\n@@ -1,5 +1,8 @@\n+# -*- coding: utf-8 -*-\n+\n __copyright__ = \"\"\"\n Copyright (C) 2006, Catalin Marinas <catalin.marinas@gmail.com>\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@@ -15,133 +18,67 @@ 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-import sys, os, time\n-from optparse import OptionParser, make_option\n-from pydoc import pager\n-from stgit.commands.common import *\n-from stgit import stack, git\n-from stgit.out import *\n-from stgit.run import Run\n+import os.path\n+from optparse import make_option\n+from stgit import run\n+from stgit.commands import common\n+from stgit.lib import log\n+from stgit.out import out\n \n help = 'display the patch changelog'\n-usage = \"\"\"%prog [options] [patch]\n-\n-List all the current and past commit ids of the given patch. The\n---graphical option invokes gitk instead of printing. The changelog\n-commit messages have the form '<action> <new-patch-id>'. The <action>\n-can be one of the following:\n+usage = \"\"\"%prog [options] [<patchnames>]\n \n-  new     - new patch created\n-  refresh - local changes were added to the patch\n-  push    - the patch was cleanly pushed onto the stack\n-  push(m) - the patch was pushed onto the stack with a three-way merge\n-  push(f) - the patch was fast-forwarded\n-  undo    - the patch boundaries were restored to the old values\n+List the history of the patch stack: the stack log. If one or more\n+patch names are given, limit the list to the log entries that touch\n+the named patches.\n \n-Note that only the diffs shown in the 'refresh', 'undo' and 'sync'\n-actions are meaningful for the patch changes. The 'push' actions\n-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+\"stg undo\" and \"stg redo\" let you step back and forth in the patch\n+stack. \"stg reset\" lets you go directly to any state.\"\"\"\n \n-directory = DirectoryHasRepository(log = False)\n+directory = common.DirectoryHasRepositoryLib()\n options = [make_option('-b', '--branch',\n                        help = 'use BRANCH instead of the default one'),\n-           make_option('-p', '--patch',\n-                       help = 'show the refresh diffs',\n-                       action = 'store_true'),\n+           make_option('-d', '--diff', action = 'store_true',\n+                       help = 'show diff to previous state'),\n            make_option('-n', '--number', type = 'int',\n-                       help = 'limit the output to NUMBER commits'),\n-           make_option('-f', '--full',\n-                       help = 'show the full commit ids',\n-                       action = 'store_true'),\n-           make_option('-g', '--graphical',\n-                       help = 'run gitk instead of printing',\n-                       action = 'store_true')]\n-\n-def show_log(log, options):\n-    \"\"\"List the patch changelog\n-    \"\"\"\n-    commit = git.get_commit(log)\n-    if options.number != None:\n-        n = options.number\n-    else:\n-        n = -1\n-    diff_list = []\n-    while commit:\n-        if n == 0:\n-            # limit the output\n-            break\n-\n-        log = commit.get_log().split('\\n')\n-\n-        cmd_rev = log[0].split()\n-        if len(cmd_rev) >= 2:\n-            cmd = cmd_rev[0]\n-            rev = cmd_rev[1]\n-        elif len(cmd_rev) == 1:\n-            cmd = cmd_rev[0]\n-            rev = ''\n-        else:\n-            cmd = rev = ''\n-\n-        if options.patch:\n-            if cmd in ['refresh', 'undo', 'sync', 'edit']:\n-                diff_list.append(git.pretty_commit(commit.get_id_hash()))\n-\n-                # limiter decrement\n-                n -= 1\n-        else:\n-            if len(log) >= 3:\n-                notes = log[2]\n-            else:\n-                notes = ''\n-            author_name, author_email, author_date = \\\n-                         name_email_date(commit.get_author())\n-            secs, tz = author_date.split()\n-            date = '%s %s' % (time.ctime(int(secs)), tz)\n-\n-            if options.full:\n-                out.stdout('%-7s %-40s %s' % (cmd[:7], rev[:40], date))\n-            else:\n-                out.stdout('%-8s [%-7s] %-28s  %s' % \\\n-                           (rev[:8], cmd[:7], notes[:28], date))\n-\n-            # limiter decrement\n-            n -= 1\n-\n-        parent = commit.get_parent()\n-        if parent:\n-            commit = git.get_commit(parent)\n-        else:\n-            commit = None\n-\n-    if options.patch and diff_list:\n-        pager('\\n'.join(diff_list).rstrip())\n+                       help = 'limit the output to NUMBER entries'),\n+           make_option('-f', '--full', action = 'store_true',\n+                       help = 'show the log in more detail'),\n+           make_option('-g', '--graphical', action = 'store_true',\n+                       help = 'run gitk instead of printing')]\n+\n+def show_log(stacklog, pathlim, num, full, show_diff):\n+    cmd = ['git', 'log']\n+    if num != None and num > 0:\n+        cmd.append('-%d' % num)\n+    if show_diff:\n+        cmd.append('-p')\n+    elif not full:\n+        cmd.append('--pretty=format:%h   %aD   %s')\n+    run.Run(*(cmd + [stacklog.sha1, '--'] + pathlim)).run()\n \n def func(parser, options, args):\n-    \"\"\"Show the patch changelog\n-    \"\"\"\n-    if len(args) == 0:\n-        name = crt_series.get_current()\n-        if not name:\n-            raise CmdException, 'No patches applied'\n-    elif len(args) == 1:\n-        name = args[0]\n-        if not name in crt_series.get_applied() + crt_series.get_unapplied() + \\\n-               crt_series.get_hidden():\n-            raise CmdException, 'Unknown patch \"%s\"' % name\n+    if options.branch:\n+        stack = directory.repository.get_stack(options.branch)\n     else:\n-        parser.error('incorrect number of arguments')\n-\n-    patch = crt_series.get_patch(name)\n-\n-    log = patch.get_log()\n-    if not log:\n-        raise CmdException, 'No changelog for patch \"%s\"' % name\n+        stack = directory.repository.current_stack\n+    patches = common.parse_patches(args, list(stack.patchorder.all))\n+    logref = log.log_ref(stack.name)\n+    try:\n+        logcommit = stack.repository.refs.get(logref)\n+    except KeyError:\n+        out.info('Log is empty')\n+        return\n+    stacklog = log.Log(stack.repository, logref, logcommit)\n+    pathlim = [os.path.join('patches', pn) for pn in patches]\n \n     if options.graphical:\n-        # discard the exit codes generated by SIGINT, SIGKILL, SIGTERM\n-        Run('gitk', log).returns([0, -2, -9, -15]).run()\n+        for o in ['diff', 'number', 'full']:\n+            if getattr(options, o):\n+                parser.error('cannot combine --graphical and --%s' % o)\n+        # Discard the exit codes generated by SIGINT, SIGKILL, and SIGTERM.\n+        run.Run(*(['gitk', stacklog.pretty.sha1, '--'] + pathlim)\n+                 ).returns([0, -2, -9, -15]).run()\n     else:\n-        show_log(log, options)\n+        show_log(stacklog.pretty, pathlim,\n+                 options.number, options.full, options.diff)\ndiff --git a/stgit/commands/reset.py b/stgit/commands/reset.py\nindex 22d7731..bbe5dda 100644\n--- a/stgit/commands/reset.py\n+++ b/stgit/commands/reset.py\n@@ -26,19 +26,8 @@ help = 'reset the patch stack to an earlier state'\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-log in foo.stgit^. So to undo the last N StGit commands (or rather,\n-the last N log entries; there is not an exact one-to-one\n-relationship), 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+a commit id from a stack log; \"stg log\" lets you view this log, and\n+\"stg reset\" lets you reset to any state 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.\"\"\"\ndiff --git a/stgit/lib/log.py b/stgit/lib/log.py\nindex 8c0516b..61e9cf2 100644\n--- a/stgit/lib/log.py\n+++ b/stgit/lib/log.py\n@@ -270,6 +270,9 @@ class Log(object):\n     def parents(self):\n         num_refs = len(set([self.top, self.head]))\n         return self.commit.data.parents[(1 + num_refs):]\n+    @property\n+    def pretty(self):\n+        return self.commit.data.parents[0]\n \n class FullLog(Log):\n     full_log = property(lambda self: self.commit)\ndiff --git a/t/t1400-patch-history.sh b/t/t1400-patch-history.sh\ndeleted file mode 100755\nindex a693e75..0000000\n--- a/t/t1400-patch-history.sh\n+++ /dev/null\n@@ -1,103 +0,0 @@\n-#!/bin/sh\n-#\n-# Copyright (c) 2006 Catalin Marinas\n-#\n-\n-test_description='Test the patch history generation.\n-\n-'\n-\n-. ./test-lib.sh\n-\n-test_expect_success \\\n-\t'Initialize the StGIT repository' \\\n-\t'\n-\tstg init\n-\t'\n-\n-test_expect_success \\\n-\t'Create the first patch' \\\n-\t'\n-\tstg new foo -m \"Foo Patch\" &&\n-\techo foo > test && echo foo2 >> test &&\n-\tgit add test &&\n-\tstg refresh --annotate=\"foo notes\"\n-\t'\n-\n-test_expect_success \\\n-\t'Create the second patch' \\\n-\t'\n-\tstg new bar -m \"Bar Patch\" &&\n-\techo bar >> test &&\n-\tstg refresh\n-\t'\n-\n-test_expect_success \\\n-\t'Check the \"new\" and \"refresh\" logs' \\\n-\t'\n-\tstg log --full foo | grep -q -e \"^refresh\" &&\n-\tstg log --full | grep -q -e \"^refresh\"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the log annotation' \\\n-\t'\n-\tstg log foo | grep -q -e    \"\\[refresh\\] foo notes  \" &&\n-\tstg log bar | grep -q -e    \"\\[refresh\\]            \" &&\n-\tstg refresh -p foo --annotate=\"foo notes 2\" &&\n-\tstg log foo | grep -q -v -e \"\\[refresh\\] foo notes  \" &&\n-\tstg log foo | grep -q -e    \"\\[refresh\\] foo notes 2\"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the \"push\" log' \\\n-\t'\n-\tstg pop &&\n-\techo foo > test2 && git add test2 && stg refresh &&\n-\tstg push &&\n-\tstg log --full | grep -q -e \"^push    \"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the \"push(f)\" log' \\\n-\t'\n-\tstg pop &&\n-\tstg edit -m \"Foo2 Patch\" &&\n-\tstg push &&\n-\tstg log --full | grep -q -e \"^push(f) \"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the \"push(m)\" log' \\\n-\t'\n-\tstg pop &&\n-\techo foo2 > test && stg refresh &&\n-\tstg push &&\n-\tstg log --full | grep -q -e \"^push(m) \"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the \"push(c)\" log' \\\n-\t'\n-\techo bar > test && stg refresh &&\n-\tstg pop &&\n-\techo foo > test && stg refresh &&\n-\t! stg push &&\n-\tstg log --full | grep -q -e \"^push(c) \"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the push \"undo\" log' \\\n-\t'\n-\tstg push --undo &&\n-\tstg log --full bar | grep -q -e \"^undo    \"\n-\t'\n-\n-test_expect_success \\\n-\t'Check the refresh \"undo\" log' \\\n-\t'\n-\tstg refresh --undo &&\n-\tstg log --full | grep -q -e \"^undo    \"\n-\t'\n-\n-test_done\n"},{"id":"80125","messageId":"b0943d9e0806170324j12605a55m41b582ad09925cce@mail.gmail.com","threadId":"13908","inReplyTo":"20080612053424.23549.64457.stgit@yoghurt","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-17T10:24:53Z","receivedAt":"2008-06-17T10:24:53Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"Hi Karl,\n\nThere are some comments below but I think I haven't fully understood this.\n\n2008/6/12 Karl Hasselström <kha@treskal.com>:\n> Create a log branch (called <branchname>.stgit) for each StGit branch,\n> and write to it whenever the stack is modified.\n>\n> Commands using the new infrastructure write to the log when they\n> commit a transaction. Commands using the old infrastructure get a log\n> entry write written for them when they exit, unless they explicitly\n> ask for this not to happen.\n\nI was more thinking for this to be the default for backwards API compatibility.\n\n>  class _Directory(object):\n> -    def __init__(self, needs_current_series = True):\n> +    def __init__(self, needs_current_series = True, log = True):\n\ni.e. we make log = False here by default.\n\n> --- /dev/null\n> +++ b/stgit/lib/log.py\n> @@ -0,0 +1,254 @@\n> +r\"\"\"This module contains functions and classes for manipulating\n\nWhy does this start with an 'r'? I thought this is for regular expressions.\n\n> +A stack log is a git branch. Each commit contains the complete state\n> +of the stack at the moment it was written; the most recent commit has\n> +the most recent state.\n> +\n> +For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.\n\nThe main question. Is this history preserved after a git-gc?\n\n> +Tree\n> +----\n> +\n> +The top-level tree object has the following entries:\n> +\n> +  - C{version}: a blob containing the string \"C{5\\n}\".\n> +\n> +  - C{head}: a blob containing the ASCII hex sha1 of the current HEAD,\n> +    followed by a newline.\n> +\n> +  - C{applied}, C{unapplied}: blobs containing one line for each\n> +    applied or unapplied patch, in order. Each line consists of\n> +\n> +      - the ASCII hex sha1 of the patch's commit object,\n> +\n> +      - a space, and\n> +\n> +      - the patch's name.\n> +\n> +  - C{patches}: a tree containing one subtree for each patch, named\n> +    after that patch. Each such subtree contains:\n> +\n> +      - C{a}, C{b}: the patch's I{bottom} and I{top} trees.\n> +\n> +      - C{info}: a blob containing::\n> +\n> +          Author: <author name and e-mail>\n> +          Date: <patch timestamp>\n> +\n> +          <commit message>\n\nI might not fully understand this but can we not store just the commit\nobject if the patch, which would have the bottom/top information.\n\n> +Simplified log\n> +--------------\n> +\n> +The simplified log looks exactly like the normal, or I{full}, log,\n> +except for the following:\n> +\n> +  - Instead of having a tree per patch in the C{patches} subtree, it\n> +    has a blob per patch. This blob contains::\n> +\n> +      Bottom: <sha1 of patch's bottom tree>\n> +      Top:    <sha1 of patch's top tree>\n> +      Author: <author name and e-mail>\n> +      Date:   <patch timestamp>\n\nCan we not point to the original commit corresponding to the patch? It\nalready has this information.\n\n> +\n> +      <commit message>\n> +\n> +      ---\n> +\n> +      <patch's diff>\n\nWhat is the patch's diff here? Cannot it be determined from the trees?\n\n> +The simplified log contains no information not in the full log; its\n> +purpose is ease of visualization.\"\"\"\n\nAh, OK. But I think it would be more useful to see the diff between\nsubsequent revisions of a stack rather than the full patch diff.\n\nThanks.\n\n-- \nCatalin\n"},{"id":"80135","messageId":"20080617123138.GA6932@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806170324j12605a55m41b582ad09925cce@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-17T12:31:38Z","receivedAt":"2008-06-17T12:31:38Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n\n> There are some comments below but I think I haven't fully understood\n> this.\n\nI've done my best to answer. Though no doubt you'll have follow-up\nquestions.\n\n> 2008/6/12 Karl Hasselström <kha@treskal.com>:\n>\n> > Create a log branch (called <branchname>.stgit) for each StGit\n> > branch, and write to it whenever the stack is modified.\n> >\n> > Commands using the new infrastructure write to the log when they\n> > commit a transaction. Commands using the old infrastructure get a\n> > log entry write written for them when they exit, unless they\n> > explicitly ask for this not to happen.\n>\n> I was more thinking for this to be the default for backwards API\n> compatibility.\n>\n> >  class _Directory(object):\n> > -    def __init__(self, needs_current_series = True):\n> > +    def __init__(self, needs_current_series = True, log = True):\n>\n> i.e. we make log = False here by default.\n\nI might not have understood precisely what you meant; but I don't\nthink API backwards compatibilty should be an issue here? I simply fix\nall callers. If log should default to true or false is immaterial --\nit just means some extra text in one or the other of two equally\ncommon cases.\n\n> > --- /dev/null\n> > +++ b/stgit/lib/log.py\n> > @@ -0,0 +1,254 @@\n> > +r\"\"\"This module contains functions and classes for manipulating\n>\n> Why does this start with an 'r'? I thought this is for regular\n> expressions.\n\n\"r\" in front of a string literal means \"raw\" (or some such). Escape\nsequences aren't recognized inside a raw string -- e.g., r'\\n' ==\n'\\\\n'. They are useful when you have to write strings with embedded\nbackslashes, such as regexes -- or this string, which has \\n in it.\n\n> > +A stack log is a git branch. Each commit contains the complete state\n> > +of the stack at the moment it was written; the most recent commit has\n> > +the most recent state.\n> > +\n> > +For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.\n>\n> The main question. Is this history preserved after a git-gc?\n\nYes. It's stored in a regular git branch. (The design is such that it\nshould even be possible to pull a stack log from another repository\nand _still_ get everything you need.)\n\nThis should probably be in the description text.\n\n> > +  - C{patches}: a tree containing one subtree for each patch, named\n> > +    after that patch. Each such subtree contains:\n> > +\n> > +      - C{a}, C{b}: the patch's I{bottom} and I{top} trees.\n> > +\n> > +      - C{info}: a blob containing::\n> > +\n> > +          Author: <author name and e-mail>\n> > +          Date: <patch timestamp>\n> > +\n> > +          <commit message>\n>\n> I might not fully understand this but can we not store just the\n> commit object if the patch, which would have the bottom/top\n> information.\n\nYou can't store a commit object in a tree. (Well, with submodules you\ncan, but said commit object isn't protected from gc and won't be\nincluded when pulling.) The idea with this format is that with the two\ntrees and the info file, you can recreate the patch's commit -- not\nexactly, but close enough as makes no difference.\n\n> > +Simplified log\n> > +--------------\n> > +\n> > +The simplified log looks exactly like the normal, or I{full}, log,\n> > +except for the following:\n> > +\n> > +  - Instead of having a tree per patch in the C{patches} subtree, it\n> > +    has a blob per patch. This blob contains::\n> > +\n> > +      Bottom: <sha1 of patch's bottom tree>\n> > +      Top:    <sha1 of patch's top tree>\n> > +      Author: <author name and e-mail>\n> > +      Date:   <patch timestamp>\n>\n> Can we not point to the original commit corresponding to the patch?\n> It already has this information.\n>\n> > +\n> > +      <commit message>\n> > +\n> > +      ---\n> > +\n> > +      <patch's diff>\n>\n> What is the patch's diff here? Cannot it be determined from the\n> trees?\n>\n> > +The simplified log contains no information not in the full log; its\n> > +purpose is ease of visualization.\"\"\"\n>\n> Ah, OK. But I think it would be more useful to see the diff between\n> subsequent revisions of a stack rather than the full patch diff.\n\nHave you tried looking at a patch stack log (in gitk, say)? That is,\n\"stg log -g\" in this patch series. It shows you diffs between\nsubsequent revisions of the simplified log. I'm sure it's far from\nperfect, but I think it's actually quite useful.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80136","messageId":"20080617125550.GB6932@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"20080617123138.GA6932@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-17T12:55:50Z","receivedAt":"2008-06-17T12:55:50Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-17 14:31:38 +0200, Karl Hasselström wrote:\n\n> Have you tried looking at a patch stack log (in gitk, say)? That is,\n> \"stg log -g\" in this patch series. It shows you diffs between\n> subsequent revisions of the simplified log. I'm sure it's far from\n> perfect, but I think it's actually quite useful.\n\nOh -- just be sure to use a colorized diff (which you get by default\nin gitk, but not in git log, which is what stg log without -g ends up\ncalling). Without colors, a diff of two diffs is too hard to read, at\nleast for me.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80139","messageId":"b0943d9e0806170711w6da8e841p3ac83a59a81f6577@mail.gmail.com","threadId":"13908","inReplyTo":"20080617123138.GA6932@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-17T14:11:42Z","receivedAt":"2008-06-17T14:11:42Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/17 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n>> 2008/6/12 Karl Hasselström <kha@treskal.com>:\n>> >  class _Directory(object):\n>> > -    def __init__(self, needs_current_series = True):\n>> > +    def __init__(self, needs_current_series = True, log = True):\n>>\n>> i.e. we make log = False here by default.\n>\n> I might not have understood precisely what you meant; but I don't\n> think API backwards compatibilty should be an issue here? I simply fix\n> all callers. If log should default to true or false is immaterial --\n> it just means some extra text in one or the other of two equally\n> common cases.\n\nNot an issue, I just favour the existing one when the two cases are\nalmost equal.\n\n>> > --- /dev/null\n>> > +++ b/stgit/lib/log.py\n>> > @@ -0,0 +1,254 @@\n>> > +r\"\"\"This module contains functions and classes for manipulating\n>>\n>> Why does this start with an 'r'? I thought this is for regular\n>> expressions.\n>\n> \"r\" in front of a string literal means \"raw\" (or some such). Escape\n> sequences aren't recognized inside a raw string -- e.g., r'\\n' ==\n> '\\\\n'. They are useful when you have to write strings with embedded\n> backslashes, such as regexes -- or this string, which has \\n in it.\n\nThanks, I didn't know this.\n\n>> > +A stack log is a git branch. Each commit contains the complete state\n>> > +of the stack at the moment it was written; the most recent commit has\n>> > +the most recent state.\n>> > +\n>> > +For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.\n>>\n>> The main question. Is this history preserved after a git-gc?\n>\n> Yes. It's stored in a regular git branch. (The design is such that it\n> should even be possible to pull a stack log from another repository\n> and _still_ get everything you need.)\n\nBut how are the patches recreated when undoing (the\nrefs/patches/<branch>/* files)? Using the Bottom/Top tree ids that a\npatch had in the past? Are these trees still present after a git-gc?\n\n>> > +  - C{patches}: a tree containing one subtree for each patch, named\n>> > +    after that patch. Each such subtree contains:\n>> > +\n>> > +      - C{a}, C{b}: the patch's I{bottom} and I{top} trees.\n>> > +\n>> > +      - C{info}: a blob containing::\n>> > +\n>> > +          Author: <author name and e-mail>\n>> > +          Date: <patch timestamp>\n>> > +\n>> > +          <commit message>\n>>\n>> I might not fully understand this but can we not store just the\n>> commit object if the patch, which would have the bottom/top\n>> information.\n>\n> You can't store a commit object in a tree. (Well, with submodules you\n> can, but said commit object isn't protected from gc and won't be\n> included when pulling.) The idea with this format is that with the two\n> trees and the info file, you can recreate the patch's commit -- not\n> exactly, but close enough as makes no difference.\n\nWhat I meant is the SHA1 value of the patch commit instead of\nBottom/Top, Author and Date. The corresponding commit object has all\nthis information.\n\n>> > +The simplified log contains no information not in the full log; its\n>> > +purpose is ease of visualization.\"\"\"\n>>\n>> Ah, OK. But I think it would be more useful to see the diff between\n>> subsequent revisions of a stack rather than the full patch diff.\n>\n> Have you tried looking at a patch stack log (in gitk, say)?\n\nI tried \"gitk master.stgit\" and got scared :-)\n\n> That is, \"stg log -g\" in this patch series.\n\nThis is more readable.\n\n> It shows you diffs between\n> subsequent revisions of the simplified log. I'm sure it's far from\n> perfect, but I think it's actually quite useful.\n\nIt is useful, though it might take a bit of time to get used to it. It\nmight also be a bit difficult if you want to revert some changes to a\nsingle patch but not do a full stack undo which would affect other\npatches.\n\n-- \nCatalin\n"},{"id":"80147","messageId":"20080617153247.GA12520@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806170711w6da8e841p3ac83a59a81f6577@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-17T15:32:47Z","receivedAt":"2008-06-17T15:32:47Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-17 15:11:42 +0100, Catalin Marinas wrote:\n\n> 2008/6/17 Karl Hasselström <kha@treskal.com>:\n>\n> > On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n> >\n> > > 2008/6/12 Karl Hasselström <kha@treskal.com>:\n> > >\n> > > >  class _Directory(object):\n> > > > -    def __init__(self, needs_current_series = True):\n> > > > +    def __init__(self, needs_current_series = True, log = True):\n> > >\n> > > i.e. we make log = False here by default.\n> >\n> > I might not have understood precisely what you meant; but I don't\n> > think API backwards compatibilty should be an issue here? I simply\n> > fix all callers. If log should default to true or false is\n> > immaterial -- it just means some extra text in one or the other of\n> > two equally common cases.\n>\n> Not an issue, I just favour the existing one when the two cases are\n> almost equal.\n\nFair enough. I'll change it.\n\n> > > > +A stack log is a git branch. Each commit contains the complete state\n> > > > +of the stack at the moment it was written; the most recent commit has\n> > > > +the most recent state.\n> > > > +\n> > > > +For a branch C{I{foo}}, the stack log is stored in C{I{foo}.stgit}.\n> > >\n> > > The main question. Is this history preserved after a git-gc?\n> >\n> > Yes. It's stored in a regular git branch. (The design is such that it\n> > should even be possible to pull a stack log from another repository\n> > and _still_ get everything you need.)\n>\n> But how are the patches recreated when undoing (the\n> refs/patches/<branch>/* files)? Using the Bottom/Top tree ids that a\n> patch had in the past? Are these trees still present after a git-gc?\n\nThe log actually _contains_ those trees, so there is no problem.\n\n(Relatedly: I've still not written the code that recreates a patch\nfrom the trees and info if its old commit object is no longer present,\nbut it should be straightforward -- there's even a TODO at the right\nplace in the code.)\n\n> > > > +  - C{patches}: a tree containing one subtree for each patch, named\n> > > > +    after that patch. Each such subtree contains:\n> > > > +\n> > > > +      - C{a}, C{b}: the patch's I{bottom} and I{top} trees.\n> > > > +\n> > > > +      - C{info}: a blob containing::\n> > > > +\n> > > > +          Author: <author name and e-mail>\n> > > > +          Date: <patch timestamp>\n> > > > +\n> > > > +          <commit message>\n> > >\n> > > I might not fully understand this but can we not store just the\n> > > commit object if the patch, which would have the bottom/top\n> > > information.\n> >\n> > You can't store a commit object in a tree. (Well, with submodules\n> > you can, but said commit object isn't protected from gc and won't\n> > be included when pulling.) The idea with this format is that with\n> > the two trees and the info file, you can recreate the patch's\n> > commit -- not exactly, but close enough as makes no difference.\n>\n> What I meant is the SHA1 value of the patch commit instead of\n> Bottom/Top, Author and Date. The corresponding commit object has all\n> this information.\n\nI think this question is settled by my answer above: the log actually\ncontains the trees in question, which it cannot do with the commit\nobject.\n\nThe applied and unapplied files do contain the sha1s of the patches'\ncommit objects, since most of the time they will still be there, and\nwe can just re-use them. But gc and pull don't \"see\" this kind of\npointer, so we can't rely on it.\n\n> > > > +The simplified log contains no information not in the full log; its\n> > > > +purpose is ease of visualization.\"\"\"\n> > >\n> > > Ah, OK. But I think it would be more useful to see the diff\n> > > between subsequent revisions of a stack rather than the full\n> > > patch diff.\n> >\n> > Have you tried looking at a patch stack log (in gitk, say)?\n>\n> I tried \"gitk master.stgit\" and got scared :-)\n\nYou want gitk master.stgit^, which is the simplified log. Can you\nguess why I added it? :-)\n\nThe full log unfortunately has to look like hell, because it needs\nboth the stack top and the previous log entry as ancestors (since\nancestry is the only way to point to other commit objects that\nsurvives gc and cloning).\n\n> > That is, \"stg log -g\" in this patch series.\n>\n> This is more readable.\n\n(This is just gitk <branch>.stgit^.)\n\n> > It shows you diffs between subsequent revisions of the simplified\n> > log. I'm sure it's far from perfect, but I think it's actually\n> > quite useful.\n>\n> It is useful, though it might take a bit of time to get used to it.\n\nYes, much like diffs take some time to get used to if you haven't seen\nthem before.\n\nIf you have ideas for a better way to visualize this, I'm all ears.\n\n> It might also be a bit difficult if you want to revert some changes\n> to a single patch but not do a full stack undo which would affect\n> other patches.\n\nThe reset command can already reset just a single patch and not all of\nthem.\n\nI agree that a \"revert\"-style command, which undoes just one update\nand not everything that happened since then (for the entire stack or\njust a single patch), is an intriguing idea. I haven't thought about\nit much, but I'm sure it's doable. (In fact, it should be equivalent\nto patch stack merging, which I wrote a post about some time ago if\nyou recall.)\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80218","messageId":"b0943d9e0806180603h59187f7epc5014f36d070cec7@mail.gmail.com","threadId":"13908","inReplyTo":"20080617153247.GA12520@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-18T13:03:51Z","receivedAt":"2008-06-18T13:03:51Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/17 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-17 15:11:42 +0100, Catalin Marinas wrote:\n>\n>> 2008/6/17 Karl Hasselström <kha@treskal.com>:\n>>\n>> > On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n>> > > The main question. Is this history preserved after a git-gc?\n>> >\n>> > Yes. It's stored in a regular git branch. (The design is such that it\n>> > should even be possible to pull a stack log from another repository\n>> > and _still_ get everything you need.)\n>>\n>> But how are the patches recreated when undoing (the\n>> refs/patches/<branch>/* files)? Using the Bottom/Top tree ids that a\n>> patch had in the past? Are these trees still present after a git-gc?\n>\n> The log actually _contains_ those trees, so there is no problem.\n\nOK, I begin to understand. It is a generic solution for storing\nmetadata but IMHO it is too overweight when we only need a list of\napplied and unapplied files (we can remove hidden) that could be\neasily stored in a commit log. It would be useful to run a quick\nbenchmark with many (hundreds) of patches and compare it with no\nlogging variant (or the current patch logging, which isn't as\nadvanced).\n\nCould we not make this (much) simpler? I.e. <branch>.stgit is a commit\nobject whose tree is <branch>^{tree} and the message contains the\ncommand followed by list of patches in the form \"<commit> <patch>\"?\nThis commit can have two parents - the previous <branch>.stgit and\ncurrent <branch> head. All the patches are referred via the <branch>\nhead or the previous <branch> heads if unapplied (assuming that when a\npatch is created it is first applied and then popped, maybe this needs\na bit of thinking). This way, a diff between subsequent <branch>.stgit\ncommits would show the tree changes. The 'stg log' command could be\nmade to show differences in the commit messages.\n\n>> > It shows you diffs between subsequent revisions of the simplified\n>> > log. I'm sure it's far from perfect, but I think it's actually\n>> > quite useful.\n>>\n>> It is useful, though it might take a bit of time to get used to it.\n>\n> Yes, much like diffs take some time to get used to if you haven't seen\n> them before.\n>\n> If you have ideas for a better way to visualize this, I'm all ears.\n\nThe diff of diffs is generally useful but for 'refresh' logs, you can\nshow the diff between the old top tree and the new one (the current\npatch log implementation does something like this but it doesn't show\nanything for commands like 'push' where a diff of diffs would be more\nappropriate).\n\n-- \nCatalin\n"},{"id":"80220","messageId":"20080618143633.GB30540@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806180603h59187f7epc5014f36d070cec7@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-18T14:36:33Z","receivedAt":"2008-06-18T14:36:33Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-18 14:03:51 +0100, Catalin Marinas wrote:\n\n> 2008/6/17 Karl Hasselström <kha@treskal.com>:\n>\n> > The log actually _contains_ those trees, so there is no problem.\n>\n> OK, I begin to understand. It is a generic solution for storing\n> metadata\n\nI don't know about \"generic\" -- it's made specifically for storing an\nStGit stack. You could certainly store just about anything in a git\ntree, but that's also true for a git commit message ...\n\n> but IMHO it is too overweight when we only need a list of applied\n> and unapplied files (we can remove hidden) that could be easily\n> stored in a commit log.\n\nDo you mean remove hidden patches from StGit altogether, or just not\nstore them in the log?\n\n> It would be useful to run a quick benchmark with many (hundreds) of\n> patches and compare it with no logging variant (or the current patch\n> logging, which isn't as advanced).\n\nWill do, as soon as I've done some basic optimization. (A simple\n\"dirty\" flag on each patch will enable us to only write out the log\nfor the patches that have actually changed, and reuse the rest from\nthe previous log entry.)\n\n> Could we not make this (much) simpler? I.e. <branch>.stgit is a\n> commit object whose tree is <branch>^{tree} and the message contains\n> the command followed by list of patches in the form \"<commit>\n> <patch>\"? This commit can have two parents - the previous\n> <branch>.stgit and current <branch> head. All the patches are\n> referred via the <branch> head or the previous <branch> heads if\n> unapplied (assuming that when a patch is created it is first applied\n> and then popped, maybe this needs a bit of thinking).\n\nI considered a scheme much like this, but besides the problem with\n(the very few) patches that are created unapplied, it fails badly in a\nvery important corner case: when you start logging in a pre-existing\nstack. A similar failure will occur if we ever build some sort of log\npruning (without which the log will grow without limit).\n\nI suppose it would be possible to special-case the very first log\nentry: let it have all patches as ancestors. But I don't really see\nthis format as being simpler than the one I use now: all you've done,\nbasically, is cram all the data into the commit message instead of\nstoring it in the tree. (Benchmarking could certainly prove your\ndesign superior to mine, however.)\n\n> This way, a diff between subsequent <branch>.stgit commits would\n> show the tree changes. The 'stg log' command could be made to show\n> differences in the commit messages.\n\nTree changes are only really useful for refresh. (More on this below.)\n\n> > If you have ideas for a better way to visualize this, I'm all\n> > ears.\n>\n> The diff of diffs is generally useful but for 'refresh' logs, you\n> can show the diff between the old top tree and the new one (the\n> current patch log implementation does something like this but it\n> doesn't show anything for commands like 'push' where a diff of diffs\n> would be more appropriate).\n\nThe diff-of-diffs view is actually very useful for refresh as well.\nThe diff of old and new top tree is useful only for refresh (but it\n_is_ useful -- I'm not arguing against it).\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80228","messageId":"b0943d9e0806180916m4af3970ck347408661e95663@mail.gmail.com","threadId":"13908","inReplyTo":"20080618143633.GB30540@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-18T16:16:10Z","receivedAt":"2008-06-18T16:16:10Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/18 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-18 14:03:51 +0100, Catalin Marinas wrote:\n>\n>> 2008/6/17 Karl Hasselström <kha@treskal.com>:\n>>\n>> > The log actually _contains_ those trees, so there is no problem.\n>>\n>> OK, I begin to understand. It is a generic solution for storing\n>> metadata\n>\n> I don't know about \"generic\" -- it's made specifically for storing an\n> StGit stack. You could certainly store just about anything in a git\n> tree, but that's also true for a git commit message ...\n\nBy generic I meant that it is easily extensible to store other blobs\nof whatever you need. As you say, a commit message could be extensible\nas well.\n\n>> but IMHO it is too overweight when we only need a list of applied\n>> and unapplied files (we can remove hidden) that could be easily\n>> stored in a commit log.\n>\n> Do you mean remove hidden patches from StGit altogether, or just not\n> store them in the log?\n\nRemove them altogether. Is there anyone using them apart from me? I\ncould create a \"rotten\" branch and pick those patches with\n--unapplied.\n\n>> It would be useful to run a quick benchmark with many (hundreds) of\n>> patches and compare it with no logging variant (or the current patch\n>> logging, which isn't as advanced).\n>\n> Will do, as soon as I've done some basic optimization. (A simple\n> \"dirty\" flag on each patch will enable us to only write out the log\n> for the patches that have actually changed, and reuse the rest from\n> the previous log entry.)\n\nIt gets too complicated, really. A single commit with the proper\nparents could do the job. We could also easily use the commit message\ndirectly for metadata instead of .git/patches/<branch>/.\n\n>> Could we not make this (much) simpler? I.e. <branch>.stgit is a\n>> commit object whose tree is <branch>^{tree} and the message contains\n>> the command followed by list of patches in the form \"<commit>\n>> <patch>\"? This commit can have two parents - the previous\n>> <branch>.stgit and current <branch> head. All the patches are\n>> referred via the <branch> head or the previous <branch> heads if\n>> unapplied (assuming that when a patch is created it is first applied\n>> and then popped, maybe this needs a bit of thinking).\n>\n> I considered a scheme much like this, but besides the problem with\n> (the very few) patches that are created unapplied, it fails badly in a\n> very important corner case: when you start logging in a pre-existing\n> stack. A similar failure will occur if we ever build some sort of log\n> pruning (without which the log will grow without limit).\n>\n> I suppose it would be possible to special-case the very first log\n> entry: let it have all patches as ancestors. But I don't really see\n> this format as being simpler than the one I use now: all you've done,\n> basically, is cram all the data into the commit message instead of\n> storing it in the tree. (Benchmarking could certainly prove your\n> design superior to mine, however.)\n\nThe time to create that tree and blobs worries me a bit, plus the (in\nmy view) complicated structure.\n\nMaking the first log entry special gets difficult with log pruning\n(unless you prune the whole log rather than entries older than a\nchosen time or number) since you might have to re-create all the\nchained log entries as the first log's sha1 will probably change.\n\nThe applied patches are chained automatically via HEAD. For unapplied\npatches, we could add the correponding commits as parents of the\nlogging commit (starting with the third parent as the first two are\nused for log chaining and applied patches). Do we hit any OS limit\nwith the number of arguments?\n\nSince we only need the unapplied commits tracking for gc and pull\nreasons, we could only create that commit that references them when we\nprune the stack log and link it from the top one (but it won't be used\nby stgit).\n\n-- \nCatalin\n"},{"id":"80233","messageId":"20080618173246.GA1155@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806180916m4af3970ck347408661e95663@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-18T17:32:46Z","receivedAt":"2008-06-18T17:32:46Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-18 17:16:10 +0100, Catalin Marinas wrote:\n\n> 2008/6/18 Karl Hasselström <kha@treskal.com>:\n>\n> > Do you mean remove hidden patches from StGit altogether, or just\n> > not store them in the log?\n>\n> Remove them altogether. Is there anyone using them apart from me? I\n> could create a \"rotten\" branch and pick those patches with\n> --unapplied.\n\nI've never heard about anyone else using them. So fine by me!\n\n> It gets too complicated, really. A single commit with the proper\n> parents could do the job.\n\nYes. The problem is that you need quite a lot of parents. Every system\nI could come up with that got all the corner cases right was even more\ncomplex (in my eyes) than the one I ended up implementing.\n\n> We could also easily use the commit message directly for metadata\n> instead of .git/patches/<branch>/.\n\nYes. This is true for any log scheme, though. (But I agree -- longer\nterm, it would be very nice to have _only_ the log.)\n\n> The time to create that tree and blobs worries me a bit, plus the\n> (in my view) complicated structure.\n\nYou're right to worry. My log format makes things feel slightly slower\nwhen logging a 20-30 deep stack. If I can't make it faster, it's not\nviable. But I'm pretty sure I can -- it should be simple to reuse all\nthe trees and blobs for the patches that weren't touched. (And\noperations that touch lots of patches, like rebase, have to write a\nlot of git objects anyway as part of their operation, so the relative\nslowdown should not be large.)\n\nAnyway, benchmarking is the way to go here. Talk will only get us that\nfar.\n\n> Making the first log entry special gets difficult with log pruning\n> (unless you prune the whole log rather than entries older than a\n> chosen time or number) since you might have to re-create all the\n> chained log entries as the first log's sha1 will probably change.\n\nYou have to re-create all the commits anyway, since they all are\nimmutable, and all have a back pointer.\n\n> The applied patches are chained automatically via HEAD. For\n> unapplied patches, we could add the correponding commits as parents\n> of the logging commit (starting with the third parent as the first\n> two are used for log chaining and applied patches). Do we hit any OS\n> limit with the number of arguments?\n\nNot until long after we hit git limits to the number of parents of a\ncommit. I believe the octopus merge refuses to create merges with more\nthan about 25 parents, and we probably shouldn't do more than that\neither. We'll have to do a tree of octopuses.\n\nThis is why my log format looks the way it does. :-)\n\n> Since we only need the unapplied commits tracking for gc and pull\n> reasons, we could only create that commit that references them when\n> we prune the stack log and link it from the top one (but it won't be\n> used by stgit).\n\nYes, we need to create an \"unapplied octopus\" if and only if we have\nunapplied patches that we can't prove are reachable from the stack top\nor the branch head (we have to save both, in case the user has done\nsomething such as git-committing on topp of the stack and caused head\n!= top). Which is for the first log entry, and in situations such as\n\"stg pick --unapplied\", but not for \"stg pop\" and the like.\n\nI do agree that we shouldn't try to use the octopuses to get hold of\nthe commits, though -- just to keep them reachable. We save the sha1\nalong with the patch name elsewhere in a more convenient form. (My\nproposed format does precisely this.)\n\nSo. If I got it right, your proposal is:\n\n  * Tree: just take the HEAD tree.\n\n  * Commit message: list the applied and unapplied patches with their\n    commit sha1s.\n\n  * Parents: the previous log entry; branch head; something that\n    (recusively) points to all unapplied commits, if necessary.\n\nI'd add to that:\n\n  * The stack top must be a parent too if head != top.\n\n  * The commit message must include a version number, and the branch\n    head sha1.\n\n  * I'm pretty sure we want the kind of \"simplified\" log I have in my\n    proposal. The full log in your proposal is going to look every bit\n    as ugly as the one in mine.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80304","messageId":"b0943d9e0806190224v1b6434fesd3a54443422edaeb@mail.gmail.com","threadId":"13908","inReplyTo":"20080618173246.GA1155@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-19T09:24:28Z","receivedAt":"2008-06-19T09:24:28Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/18 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-18 17:16:10 +0100, Catalin Marinas wrote:\n>> We could also easily use the commit message directly for metadata\n>> instead of .git/patches/<branch>/.\n>\n> Yes. This is true for any log scheme, though. (But I agree -- longer\n> term, it would be very nice to have _only_ the log.)\n\nIt would make collaboration using stgit stacks easier. I can start\nlooking at this once we agree on the log structure.\n\n>> The time to create that tree and blobs worries me a bit, plus the\n>> (in my view) complicated structure.\n>\n> You're right to worry. My log format makes things feel slightly slower\n> when logging a 20-30 deep stack. If I can't make it faster, it's not\n> viable. But I'm pretty sure I can -- it should be simple to reuse all\n> the trees and blobs for the patches that weren't touched. (And\n> operations that touch lots of patches, like rebase, have to write a\n> lot of git objects anyway as part of their operation, so the relative\n> slowdown should not be large.)\n>\n> Anyway, benchmarking is the way to go here. Talk will only get us that\n> far.\n\nIn the past, I ran some tests because people complained that stgit was\nslow compared to quilt:\n\nhttp://article.gmane.org/gmane.comp.version-control.git/9670\n\nAfter profiling (the stg-prof file), as expected, most of the time was\nspent in external Git calls which I tried to keep to a minimum. With\nyour approach, you probably add at least 4-5 calls to Git (just a\nguess, I haven't counted) and, with a big repository, it will be\nvisible (I have about 15 branches on my Linux kernel clone going back\nto the 2.6.12 kernel and the .git size is over 1/2GB after git-gc).\n\n>> Making the first log entry special gets difficult with log pruning\n>> (unless you prune the whole log rather than entries older than a\n>> chosen time or number) since you might have to re-create all the\n>> chained log entries as the first log's sha1 will probably change.\n>\n> You have to re-create all the commits anyway, since they all are\n> immutable, and all have a back pointer.\n\nAh, OK. So, at least initially, we should only support the full log pruning.\n\n>> The applied patches are chained automatically via HEAD. For\n>> unapplied patches, we could add the correponding commits as parents\n>> of the logging commit (starting with the third parent as the first\n>> two are used for log chaining and applied patches). Do we hit any OS\n>> limit with the number of arguments?\n>\n> Not until long after we hit git limits to the number of parents of a\n> commit. I believe the octopus merge refuses to create merges with more\n> than about 25 parents, and we probably shouldn't do more than that\n> either. We'll have to do a tree of octopuses.\n\nFor the first log only, we could chain the unapplied patches using\ncommits with 2 parents. We just need to warn people not to stare at\nthe <branch>.stgit directly :-)\n\n>> Since we only need the unapplied commits tracking for gc and pull\n>> reasons, we could only create that commit that references them when\n>> we prune the stack log and link it from the top one (but it won't be\n>> used by stgit).\n>\n> Yes, we need to create an \"unapplied octopus\" if and only if we have\n> unapplied patches that we can't prove are reachable from the stack top\n> or the branch head (we have to save both, in case the user has done\n> something such as git-committing on topp of the stack and caused head\n> != top). Which is for the first log entry, and in situations such as\n> \"stg pick --unapplied\", but not for \"stg pop\" and the like.\n\nYes, but just chain unapplied commits rather than using octopus (you\ncould try to the -mm series with 500+ patches and see what happens).\n\n> I do agree that we shouldn't try to use the octopuses to get hold of\n> the commits, though -- just to keep them reachable. We save the sha1\n> along with the patch name elsewhere in a more convenient form. (My\n> proposed format does precisely this.)\n\nYes, but you still have to refer the tree objects corresponding to a\npatch and extra work to generate the simplified log.\n\n> So. If I got it right, your proposal is:\n>\n>  * Tree: just take the HEAD tree.\n\nYes.\n\n>  * Commit message: list the applied and unapplied patches with their\n>    commit sha1s.\n\nWe don't even need to differentiate between applied and unapplied in\nthe commit message as long as they are listed in order since one of\nthe parents would represent the boundary between them. Since lib.stack\nis pretty well structured, we could later modify PatchOrder to use the\nlog.\n\n>  * Parents: the previous log entry; branch head; something that\n>    (recusively) points to all unapplied commits, if necessary.\n\nAs you pointed below, \"branch head\" should probably be the \"stack\ntop\". We don't need to track the \"branch head\" if different, just need\nto fix up the error and add the patches to the stack. And, anyway, if\none modifies the HEAD using git directly, the log will still point to\nthe top of the stack.\n\nThe third head would only be needed for the first log entry or when we\nuse pick --unapplied (in the latter, it only points to the unapplied\ncommit). The third head shouldn't be used at all in the log, just\ncreated when needed.\n\n> I'd add to that:\n>\n>  * The stack top must be a parent too if head != top.\n\nSee above, branch head would actually be the stack top.\n\n>  * The commit message must include a version number, and the branch\n>    head sha1.\n\nOK with the version number but the branch head (or stack top) is one\nof the parents already.\n\n>  * I'm pretty sure we want the kind of \"simplified\" log I have in my\n>    proposal. The full log in your proposal is going to look every bit\n>    as ugly as the one in mine.\n\nI agree it will look ugly but the simplified log adds an extra\noverhead on any stgit action. If we don't use stg log -g, a text only\nlog command could show the diff. We can add it afterwards though if it\nis fast enough.\n\n-- \nCatalin\n"},{"id":"80306","messageId":"20080619100722.GB14415@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806190224v1b6434fesd3a54443422edaeb@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-19T10:07:22Z","receivedAt":"2008-06-19T10:07:22Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-19 10:24:28 +0100, Catalin Marinas wrote:\n\n> 2008/6/18 Karl Hasselström <kha@treskal.com>:\n>\n> > On 2008-06-18 17:16:10 +0100, Catalin Marinas wrote:\n> >\n> > > We could also easily use the commit message directly for\n> > > metadata instead of .git/patches/<branch>/.\n> >\n> > Yes. This is true for any log scheme, though. (But I agree --\n> > longer term, it would be very nice to have _only_ the log.)\n>\n> It would make collaboration using stgit stacks easier. I can start\n> looking at this once we agree on the log structure.\n\nYes. Collaboration was one of the things I was thinking of when\nstarting all this. (If not for that, there would be little reason to\ntry to store the entire log under a _single_ ref.)\n\n> After profiling (the stg-prof file), as expected, most of the time\n> was spent in external Git calls which I tried to keep to a minimum.\n> With your approach, you probably add at least 4-5 calls to Git (just\n> a guess, I haven't counted) and, with a big repository, it will be\n> visible (I have about 15 branches on my Linux kernel clone going\n> back to the 2.6.12 kernel and the .git size is over 1/2GB after\n> git-gc).\n\nThe extra git calls take constant time and don't depend on the size of\nthe repository. But they _are_ a problem. (Which could be solved by\nmaking a new git command specifically for users like StGit that want\nto make a small bunch of simple object read/write commands.)\n\n> > > Making the first log entry special gets difficult with log\n> > > pruning (unless you prune the whole log rather than entries\n> > > older than a chosen time or number) since you might have to\n> > > re-create all the chained log entries as the first log's sha1\n> > > will probably change.\n> >\n> > You have to re-create all the commits anyway, since they all are\n> > immutable, and all have a back pointer.\n>\n> Ah, OK. So, at least initially, we should only support the full log\n> pruning.\n\nHow do you imagine we'd do anything except a \"full\" pruning? There are\ngrafts and shallow clones, I guess, but both of them have drawbacks.\n\n> > > The applied patches are chained automatically via HEAD. For\n> > > unapplied patches, we could add the correponding commits as\n> > > parents of the logging commit (starting with the third parent as\n> > > the first two are used for log chaining and applied patches). Do\n> > > we hit any OS limit with the number of arguments?\n> >\n> > Not until long after we hit git limits to the number of parents of\n> > a commit. I believe the octopus merge refuses to create merges\n> > with more than about 25 parents, and we probably shouldn't do more\n> > than that either. We'll have to do a tree of octopuses.\n>\n> For the first log only, we could chain the unapplied patches using\n> commits with 2 parents. We just need to warn people not to stare at\n> the <branch>.stgit directly :-)\n\nWe could make chains (or trees) of 16-parent commits -- that'd speed\nit up by a factor of 15. :-)\n\n> > > Since we only need the unapplied commits tracking for gc and\n> > > pull reasons, we could only create that commit that references\n> > > them when we prune the stack log and link it from the top one\n> > > (but it won't be used by stgit).\n> >\n> > Yes, we need to create an \"unapplied octopus\" if and only if we\n> > have unapplied patches that we can't prove are reachable from the\n> > stack top or the branch head (we have to save both, in case the\n> > user has done something such as git-committing on topp of the\n> > stack and caused head != top). Which is for the first log entry,\n> > and in situations such as \"stg pick --unapplied\", but not for \"stg\n> > pop\" and the like.\n>\n> Yes, but just chain unapplied commits rather than using octopus (you\n> could try to the -mm series with 500+ patches and see what happens).\n\nIf there are a lot of unapplied patches, you don't want to throw that\nfactor of 15 away I think.\n\n> > I do agree that we shouldn't try to use the octopuses to get hold\n> > of the commits, though -- just to keep them reachable. We save the\n> > sha1 along with the patch name elsewhere in a more convenient\n> > form. (My proposed format does precisely this.)\n>\n> Yes, but you still have to refer the tree objects corresponding to a\n> patch and extra work to generate the simplified log.\n\nTrue. But only for patches that have changed. Which will rarely be a\nlot of them, as I've said before.\n\n> >  * Commit message: list the applied and unapplied patches with\n> >    their commit sha1s.\n>\n> We don't even need to differentiate between applied and unapplied in\n> the commit message as long as they are listed in order since one of\n> the parents would represent the boundary between them.\n\nNot if top != head (which I'm convinced we want to allow the design to\nhandle). See below.\n\n> Since lib.stack is pretty well structured, we could later modify\n> PatchOrder to use the log.\n\nCertainly. This has been one of my evil master plans all along. ;-)\nThough it would have to be restructured a bit since we can't just\nwrite to a part of the log -- we have to write a complete log entry\nall at once.\n\n> >  * Parents: the previous log entry; branch head; something that\n> >    (recusively) points to all unapplied commits, if necessary.\n>\n> As you pointed below, \"branch head\" should probably be the \"stack\n> top\". We don't need to track the \"branch head\" if different, just\n> need to fix up the error and add the patches to the stack. And,\n> anyway, if one modifies the HEAD using git directly, the log will\n> still point to the top of the stack.\n\nIf we ever want to be able to undo \"stg repair\", we have to be able to\nrepresent an inconsistent state where head != top. And it's actually\nnot difficult -- the patch series I posted recently does this. All it\ntakes is that we save both the branch head and the stack top, instead\nof assuming that they are the same.\n\n> The third head would only be needed for the first log entry or when\n> we use pick --unapplied (in the latter, it only points to the\n> unapplied commit).\n\nActually, except for the previous log entry, all the parents are just\nthere for gc's benefit. So we could just put all of them in the same\nbucket -- branch head, stack top, and unapplied patches.\n\n( By \"bucket\" I mean something like: if there are just a few of them,\n  have them as direct parents of the log commit; otherwise, refer to\n  them using a tree of octopuses. But in any case, just treat them as\n  a set of sha1s that we need to have as ancestors but don't otherwise\n  care about. )\n\n> The third head shouldn't be used at all in the log, just created\n> when needed.\n\nYes. And as I said, we could do the same for the head and top commits.\n\n> >  * I'm pretty sure we want the kind of \"simplified\" log I have in\n> >    my proposal. The full log in your proposal is going to look\n> >    every bit as ugly as the one in mine.\n>\n> I agree it will look ugly but the simplified log adds an extra\n> overhead on any stgit action. If we don't use stg log -g, a text\n> only log command could show the diff. We can add it afterwards\n> though if it is fast enough.\n\nI'd actually say the opposite: until we have a good visualizer that\ndoesn't need the simplified log, we need to have the simplified log.\nIf I actually have to look at the diffs in the log, I find gitk\nindispensible.\n\nPlus, once we try to support merging logs, gitk on a simplified log\nwill be truly indispensible.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"80449","messageId":"b0943d9e0806200214j77aef272sc5cfb98b002cae22@mail.gmail.com","threadId":"13908","inReplyTo":"20080619100722.GB14415@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-06-20T09:14:29Z","receivedAt":"2008-06-20T09:14:29Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/19 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-19 10:24:28 +0100, Catalin Marinas wrote:\n>\n>> 2008/6/18 Karl Hasselström <kha@treskal.com>:\n>>\n>> > On 2008-06-18 17:16:10 +0100, Catalin Marinas wrote:\n>> > > Making the first log entry special gets difficult with log\n>> > > pruning (unless you prune the whole log rather than entries\n>> > > older than a chosen time or number) since you might have to\n>> > > re-create all the chained log entries as the first log's sha1\n>> > > will probably change.\n>> >\n>> > You have to re-create all the commits anyway, since they all are\n>> > immutable, and all have a back pointer.\n>>\n>> Ah, OK. So, at least initially, we should only support the full log\n>> pruning.\n>\n> How do you imagine we'd do anything except a \"full\" pruning? There are\n> grafts and shallow clones, I guess, but both of them have drawbacks.\n\nRecreate the log but we I wouldn't spend time on this.\n\n>> > > The applied patches are chained automatically via HEAD. For\n>> > > unapplied patches, we could add the correponding commits as\n>> > > parents of the logging commit (starting with the third parent as\n>> > > the first two are used for log chaining and applied patches). Do\n>> > > we hit any OS limit with the number of arguments?\n>> >\n>> > Not until long after we hit git limits to the number of parents of\n>> > a commit. I believe the octopus merge refuses to create merges\n>> > with more than about 25 parents, and we probably shouldn't do more\n>> > than that either. We'll have to do a tree of octopuses.\n>>\n>> For the first log only, we could chain the unapplied patches using\n>> commits with 2 parents. We just need to warn people not to stare at\n>> the <branch>.stgit directly :-)\n>\n> We could make chains (or trees) of 16-parent commits -- that'd speed\n> it up by a factor of 15. :-)\n\nYes, good idea.\n\n>> >  * Parents: the previous log entry; branch head; something that\n>> >    (recusively) points to all unapplied commits, if necessary.\n>>\n>> As you pointed below, \"branch head\" should probably be the \"stack\n>> top\". We don't need to track the \"branch head\" if different, just\n>> need to fix up the error and add the patches to the stack. And,\n>> anyway, if one modifies the HEAD using git directly, the log will\n>> still point to the top of the stack.\n>\n> If we ever want to be able to undo \"stg repair\", we have to be able to\n> represent an inconsistent state where head != top.\n\nI wouldn't bother with this feature. Why would one want to break the\nstack again after repairing? If they merge patches and git commits,\nthey either repair the stack or commit all the patches and continue\nwith using Git only.\n\n>> The third head would only be needed for the first log entry or when\n>> we use pick --unapplied (in the latter, it only points to the\n>> unapplied commit).\n>\n> Actually, except for the previous log entry, all the parents are just\n> there for gc's benefit. So we could just put all of them in the same\n> bucket -- branch head, stack top, and unapplied patches.\n>\n> ( By \"bucket\" I mean something like: if there are just a few of them,\n>  have them as direct parents of the log commit; otherwise, refer to\n>  them using a tree of octopuses. But in any case, just treat them as\n>  a set of sha1s that we need to have as ancestors but don't otherwise\n>  care about. )\n\nYes.\n\n>> >  * I'm pretty sure we want the kind of \"simplified\" log I have in\n>> >    my proposal. The full log in your proposal is going to look\n>> >    every bit as ugly as the one in mine.\n>>\n>> I agree it will look ugly but the simplified log adds an extra\n>> overhead on any stgit action. If we don't use stg log -g, a text\n>> only log command could show the diff. We can add it afterwards\n>> though if it is fast enough.\n>\n> I'd actually say the opposite: until we have a good visualizer that\n> doesn't need the simplified log, we need to have the simplified log.\n> If I actually have to look at the diffs in the log, I find gitk\n> indispensible.\n\nAnd what would the simplified log contain if we decide to go with a\nnew scheme? In your proposal, it points to the tree of main log and\nyou get the diff of diffs (which also means that the diffs must be\ngenerated for every modification of a patch). Would this be the same?\nAgain, I worry a bit about the overhead to generate the patch diff for\nevery push (with refresh I'm OK). It can be optimised as in the stable\nbranch where we try git-apply followed by a three-way merge (which,\nBTW, I'd like added before 0.15). If git-apply succeeds, there is no\nneed to re-generate the diff.\n\n-- \nCatalin\n"},{"id":"80697","messageId":"20080623123631.GA18347@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0806200214j77aef272sc5cfb98b002cae22@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-06-23T12:36:31Z","receivedAt":"2008-06-23T12:36:31Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-20 10:14:29 +0100, Catalin Marinas wrote:\n\n> 2008/6/19 Karl Hasselström <kha@treskal.com>:\n>\n> > If we ever want to be able to undo \"stg repair\", we have to be\n> > able to represent an inconsistent state where head != top.\n>\n> I wouldn't bother with this feature. Why would one want to break the\n> stack again after repairing? If they merge patches and git commits,\n> they either repair the stack or commit all the patches and continue\n> with using Git only.\n\nSometimes, stg repair isn't what you want. Say e.g. that you've done\ngit merge. stg repair will in this case stop reading your history once\nit sees the merge commit, and consider all your patches unapplied. But\nwhat you really want is to undo the git merge. Or that you've done git\nrebase -- in that case, stg repair will realize that your patches are\nunapplied, which is correct but probably not what you want.\n\nFor those of us who know what stg repair does, realizing when not to\nrun it and what to do instead is easy. But for very little extra\neffort in the stack log, we can make the generic \"stg undo\" able to\nfix these mistakes automatically. _And_ give the user assurance that\nwhatever she does with stg reset and stg undo, it can be undone.\n\n> > I'd actually say the opposite: until we have a good visualizer\n> > that doesn't need the simplified log, we need to have the\n> > simplified log. If I actually have to look at the diffs in the\n> > log, I find gitk indispensible.\n>\n> And what would the simplified log contain if we decide to go with a\n> new scheme? In your proposal, it points to the tree of main log and\n> you get the diff of diffs (which also means that the diffs must be\n> generated for every modification of a patch). Would this be the\n> same? Again, I worry a bit about the overhead to generate the patch\n> diff for every push (with refresh I'm OK). It can be optimised as in\n> the stable branch where we try git-apply followed by a three-way\n> merge (which, BTW, I'd like added before 0.15). If git-apply\n> succeeds, there is no need to re-generate the diff.\n\nYes, I was imagining a simplified log precisely like the one I've\ncurrently implemented (a tree with one blob per patch, which contains\nthe message, the diff, and some other odds and ends such as the\nauthor). Two optimizations would hopefully make it fast:\n\n  1. If the patch's sha1 hasn't changed, we don't have to regenerate\n     the diff.\n\n  2. If the patch's sha1 has changed, but git apply was sufficient\n     during the merge stage, we can just reuse that patch. We do have\n     to write it to a blob, but we have already generated the diff and\n     don't need to do so again. (I've shamelessly stolen your idea\n     here.)\n\nIn most cases, (1) would make sure that only a small handful of\npatches would need to be considered. In the cases where a lot of\npatches are touched, such as rebase, (2) would provide a good speedup\n(except for the cases where we had to call merge-recursive, and those\nare slow anyway).\n\n( By the way: Yes, I agree that we want to try git apply before\n  merge-recursive. It's on my TODO list. )\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"81902","messageId":"20080701201355.GA19096@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"20080617153247.GA12520@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-07-01T20:13:55Z","receivedAt":"2008-07-01T20:13:55Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-06-17 17:32:47 +0200, Karl Hasselström wrote:\n\n> On 2008-06-17 15:11:42 +0100, Catalin Marinas wrote:\n>\n> > 2008/6/17 Karl Hasselström <kha@treskal.com>:\n> >\n> > > On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n> > >\n> > > > 2008/6/12 Karl Hasselström <kha@treskal.com>:\n> > > >\n> > > > >  class _Directory(object):\n> > > > > -    def __init__(self, needs_current_series = True):\n> > > > > +    def __init__(self, needs_current_series = True, log = True):\n> > > >\n> > > > i.e. we make log = False here by default.\n> > >\n> > > I might not have understood precisely what you meant; but I\n> > > don't think API backwards compatibilty should be an issue here?\n> > > I simply fix all callers. If log should default to true or false\n> > > is immaterial -- it just means some extra text in one or the\n> > > other of two equally common cases.\n> >\n> > Not an issue, I just favour the existing one when the two cases\n> > are almost equal.\n>\n> Fair enough. I'll change it.\n\nI had an even better idea: no default value. Every caller gets to say\neither log = True or log = False, which makes it immediately obvious\nto the reader. (That is, every caller still using the old\ninfrastructure; with the new infrastructure, we log if and only if a\ntransaction is run.)\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"82166","messageId":"b0943d9e0807031505n5b119a6fxf0b050d72b59352a@mail.gmail.com","threadId":"13908","inReplyTo":"20080701201355.GA19096@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-07-03T22:05:45Z","receivedAt":"2008-07-03T22:05:45Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/7/1 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-17 17:32:47 +0200, Karl Hasselström wrote:\n>\n>> On 2008-06-17 15:11:42 +0100, Catalin Marinas wrote:\n>>\n>> > 2008/6/17 Karl Hasselström <kha@treskal.com>:\n>> >\n>> > > On 2008-06-17 11:24:53 +0100, Catalin Marinas wrote:\n>> > >\n>> > > > 2008/6/12 Karl Hasselström <kha@treskal.com>:\n>> > > >\n>> > > > >  class _Directory(object):\n>> > > > > -    def __init__(self, needs_current_series = True):\n>> > > > > +    def __init__(self, needs_current_series = True, log = True):\n>> > > >\n>> > > > i.e. we make log = False here by default.\n>> > >\n>> > > I might not have understood precisely what you meant; but I\n>> > > don't think API backwards compatibilty should be an issue here?\n>> > > I simply fix all callers. If log should default to true or false\n>> > > is immaterial -- it just means some extra text in one or the\n>> > > other of two equally common cases.\n>> >\n>> > Not an issue, I just favour the existing one when the two cases\n>> > are almost equal.\n>>\n>> Fair enough. I'll change it.\n>\n> I had an even better idea: no default value. Every caller gets to say\n> either log = True or log = False, which makes it immediately obvious\n> to the reader. (That is, every caller still using the old\n> infrastructure; with the new infrastructure, we log if and only if a\n> transaction is run.)\n\nFair enough.\n\n-- \nCatalin\n"},{"id":"83068","messageId":"b0943d9e0807120309jb01aa09se5749a19bc526240@mail.gmail.com","threadId":"13908","inReplyTo":"20080623123631.GA18347@diana.vm.bytemark.co.uk","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2008-07-12T10:09:18Z","receivedAt":"2008-07-12T10:09:18Z","isPatch":true,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"2008/6/23 Karl Hasselström <kha@treskal.com>:\n> On 2008-06-20 10:14:29 +0100, Catalin Marinas wrote:\n>\n>> 2008/6/19 Karl Hasselström <kha@treskal.com>:\n>> > I'd actually say the opposite: until we have a good visualizer\n>> > that doesn't need the simplified log, we need to have the\n>> > simplified log. If I actually have to look at the diffs in the\n>> > log, I find gitk indispensible.\n>>\n>> And what would the simplified log contain if we decide to go with a\n>> new scheme? In your proposal, it points to the tree of main log and\n>> you get the diff of diffs (which also means that the diffs must be\n>> generated for every modification of a patch). Would this be the\n>> same? Again, I worry a bit about the overhead to generate the patch\n>> diff for every push (with refresh I'm OK). It can be optimised as in\n>> the stable branch where we try git-apply followed by a three-way\n>> merge (which, BTW, I'd like added before 0.15). If git-apply\n>> succeeds, there is no need to re-generate the diff.\n>\n> Yes, I was imagining a simplified log precisely like the one I've\n> currently implemented (a tree with one blob per patch, which contains\n> the message, the diff, and some other odds and ends such as the\n> author).\n\nAt a first look, I'm OK with a simplified log.\n\n> Two optimizations would hopefully make it fast:\n>\n>  1. If the patch's sha1 hasn't changed, we don't have to regenerate\n>     the diff.\n>\n>  2. If the patch's sha1 has changed, but git apply was sufficient\n>     during the merge stage, we can just reuse that patch. We do have\n>     to write it to a blob, but we have already generated the diff and\n>     don't need to do so again. (I've shamelessly stolen your idea\n>     here.)\n\nIt can be optimised a bit more to actually apply the diff in the blob\ndirectly rather than the current way of generating the diff (since we\ndon't store the diff).\n\n> In most cases, (1) would make sure that only a small handful of\n> patches would need to be considered. In the cases where a lot of\n> patches are touched, such as rebase, (2) would provide a good speedup\n> (except for the cases where we had to call merge-recursive, and those\n> are slow anyway).\n\nI think it should work. Rebase is indeed my worry but it might be even\nfaster for most of the patches to apply the blob than computing the\ndiff. In my experience with the Linux kernel, full merge is rarely\nneeded.\n\n-- \nCatalin\n"},{"id":"83220","messageId":"20080714063256.GC20751@diana.vm.bytemark.co.uk","threadId":"13908","inReplyTo":"b0943d9e0807120309jb01aa09se5749a19bc526240@mail.gmail.com","subject":"Re: [StGit PATCH 03/14] Write to a stack log when stack is modified","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2008-07-14T06:32:56Z","receivedAt":"2008-07-14T06:32:56Z","isPatch":true,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2008-07-12 11:09:18 +0100, Catalin Marinas wrote:\n\n> 2008/6/23 Karl Hasselström <kha@treskal.com>:\n>\n> > Two optimizations would hopefully make it fast:\n> >\n> >  1. If the patch's sha1 hasn't changed, we don't have to\n> >     regenerate the diff.\n> >\n> >  2. If the patch's sha1 has changed, but git apply was sufficient\n> >     during the merge stage, we can just reuse that patch. We do\n> >     have to write it to a blob, but we have already generated the\n> >     diff and don't need to do so again. (I've shamelessly stolen\n> >     your idea here.)\n>\n> It can be optimised a bit more to actually apply the diff in the\n> blob directly rather than the current way of generating the diff\n> (since we don't store the diff).\n\nAh. Yes, that might be faster. While git is supposedly very fast in\ndiffing two trees, it should be even faster in just reading a blob.\n\n> > In most cases, (1) would make sure that only a small handful of\n> > patches would need to be considered. In the cases where a lot of\n> > patches are touched, such as rebase, (2) would provide a good\n> > speedup (except for the cases where we had to call\n> > merge-recursive, and those are slow anyway).\n>\n> I think it should work. Rebase is indeed my worry but it might be\n> even faster for most of the patches to apply the blob than computing\n> the diff. In my experience with the Linux kernel, full merge is\n> rarely needed.\n\nYes. Now all we need is a good benchmarking system so that we know\nwe're heading in the right direction. I need to fix up and post that\nscript I used.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"}]}