{"thread":{"id":"10477","subject":"[StGit RFC] A more structured way of calling git","startedAt":"2007-10-26T19:24:18Z","lastAt":"2007-11-16T08:36:21Z","messageCount":8,"participants":["Karl Hasselström","Catalin Marinas","Yann Dirson"],"isPatch":false,"patchVersion":null,"patchTotal":null},"messages":[{"id":"57301","messageId":"20071026192418.GA19774@diana.vm.bytemark.co.uk","threadId":"10477","inReplyTo":null,"subject":"[StGit RFC] A more structured way of calling git","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-10-26T19:24:18Z","receivedAt":"2007-10-26T19:24:18Z","isPatch":false,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"I wanted to build an StGit command that coalesced adjacent patches to\na single patch. Because the end result tree would still be the same,\nthis should be doable without ever involving HEAD, the index, or the\nworktree. StGit's existing infrastructure for manipulating patches\ndidn't lend itself to doing this kind of thing, though: it's not\nmodular enough. So I started to design a replacement low-level\ninterface to git, and things got slightly out of hand ... and I ended\nup with a much bigger refactoring than I'd planned.\n\nIt's all outlined below, and all the code I currently have is\nattached. Unless there's opposition, my plan is to convert one command\nat a time to use the new infrastructure -- this can be done since the\non-disk format is unaffected.\n\nComments?\n\n\nPython wrapping of git objects (gitlib.py)\n----------------------------------------------------------------------\n\nTo make it easier to work with git objects, I've built a more\nhigh-level interface than just calling git commands directly. Some of\nit is trivial:\n\n  * Blobs and tags aren't covered (yet), since StGit never uses them.\n    I think. If they are needed in the future, they can easily be\n    wrapped.\n\n  * Trees are represented by the Python class Tree. They are\n    immutable, and have only one property: the sha1 of the tree. More\n    could be added if we need to look inside trees.\n\nThe interesting case is for commit objects:\n\n  * Commits are represented by the Python class Commit. They are\n    immutable, and have two properties: sha1 and commit data.\n\n  * Commit data is represented by the Python class CommitData. These\n    objects are also immutable, and have properties for author,\n    committer, commit message, tree, and list of parent commits. They\n    also have setter functions for these properties, which (since\n    CommitData objects are immutable) return a modified copy of the\n    original object.\n\n  * Author and committer are represented by a Person class, also\n    immutable with setter functions.\n\nThe user may create new CommitData objects, but never creates Tree or\nCommit objects herself. Instead, she asks her Repository object to\ncreate them for her:\n\n  * Repository.get_tree and Repository.get_commit take sha1\n    parameters, and returns the corresponding objects. They must\n    already exist.\n\n  * Repository.commit takes a CommitData parameter, and returns a\n    corresponding Commit object representing the new commit.\n    Internally, it runs git-commit-tree.\n\nThis has the nice property that Tree and Commit objects always\nrepresent objects that git knows about. It also makes it trivial to\ncreate new commits that have arbitrary existing commits as parents and\nan existing tree. For example, my coalesce-adjacent-patches command\ncould be built on top of this.\n\nThe Repository object also has methods for reading and writing (and\ncaching) refs, but it lacks any method for creating new trees. This is\nthe job of the Index object. It has read_tree and write_tree methods\nfor getting Trees in to and out of the current index state. It also\nhas a merge method that does a 3-way merge between arbitrary Trees,\nwithout ever touching the worktree (if the merge cannot be resolved\nautomatically, it simply fails).\n\nThe user is free to create as many Repository and Index objects as she\nwants; their constructors take a git repository path and an index file\npath as argumentes, respectively. This means that it's very easy to\nwork with a temporary index, which is neat in combination with the\nIndex.merge method: it lets you merge three trees to create a fourth\nwithout ever touching the worktree or the default index.\n\nFor operations that involve a worktree as well, we have the\nIndexAndWorktree class. It has methods for e.g. checkout and\nupdate-index; this is also where a full, possibly conflicting merge\nwill go when I get around to implementing it.\n\n\nLow-level StGit on top of the git wrappers (stacklib.py)\n----------------------------------------------------------------------\n\nThat was all about git. We need an StGit layer on top of it.\n\nThere's a Stack object that represents a branch. It has two important\nproperties:\n\n  * A PatchOrder object. This keeps track of the list of applied and\n    unapplied patches, by name.\n\n  * A store of Patch objects. This can look up Patch objects by name,\n    and create new patches.\n\nPatch objects represent patches, and are very simple. Basically the\nonly thing you can do with them is get their commit object, set their\ncommit object, and delete them. Author, commit message, top and\nbottom, and all those things aren't a property of the patch; they are\nproperties of its commit.\n\n(In the future, Patch objects should write stuff to the patch log.\nThey could also during a gradual transition to this new infrastructure\nwrite out the per-patch metadata files that StGit currently uses.)\n\nImportantly, unlike the current StGit stack class, there are no\nhigh-level stack operations à la push and pop here. This is all\nlow-level manipulation of patch refs and the applied/unapplied files.\nBut in combination with the stuff in gitlib.py, lots of higher-level\nStGit operations can be built on top of this.\n\n\nTransactions (translib.py)\n----------------------------------------------------------------------\n\nI started to implement a few StGit commands on top of gitlib.py and\nstacklib.py, and then realized something very appealing:\n\n  Just about every StGit command can be accomplished by first creating\n  a bunch of new commit objects with gitlib.py, then trying to check\n  out the new HEAD, and then rewriting refs with stacklib.py. Only the\n  first and second steps can possibly fail, and if they do, they do so\n  without leaving any user-visible junk behind. This can be used to\n  make all commands either succeed completely, or do nothing at all.\n\nAs an example (which I've not yet implemented), consider how push\nwould work:\n\n  1. Create the new commit objects that the patches to be pushed will\n     use. For each patch:\n\n       a. Check if it's a fast forward. If so, just reuse the old\n          commit object.\n\n       b. Try the in-index merge with a temp index. If it succeeds,\n          create a new commit object with that tree.\n\n       c. Otherwise, stop trying to push any more patches.\n\n  2. Check out the new HEAD tree. This may fail if the worktree and/or\n     index contain conflicting changes. If so, we just abort the whole\n     operation and tell the user which files she needs to clean up.\n\n       a. If we had a patch that we couldn't push in (1.c), and then\n          do a full 3-way merge with its original tree. This may fail\n          if the worktree and/or index is dirty; if so, we don't try\n          to push that patch.\n\n       b. If the merge succeeds but with conflicts, create a new\n          commit for it with the same tree as its parent (i.e. an\n          empty commit) and leave the conflicts for the user to\n          resolve.\n\n       c. Otherwise, the merge autoresolved. Go back to (1) and try to\n          push the remaining patches too. But remember that if we\n          later need to abort the push due to dirty worktree/index, we\n          have already pushed a few of the patches.\n\n  3. Use stacklib.py to rewrite the branch ref and the patch refs.\n\nThis will end up pushing some subset of the requested patches. The\nonly way we'll ever get a result that isn't all-or-nothing is if a\nmerge conflicts. Note also how (except for the irritating (2.c)) we\nnever touch the index and worktree until we're already done, which\nshould make things both robust and fast.\n\n(Step (2.c) is irritating, in that we actually have to check out a new\ntree in order to use merge-recursive, and merge-recursive might\nautoresolve a merge that the in-index merge failed to resolve, so that\nwe have checked out an intermediate tree even though there didn't end\nup being any conflict for the user to resolve.)\n\nThe code in translib.py is a simple class that can hold a record of\neverything that needs to be done in step (3), and then does it when\nand if we get there.\n\nThe killer feature of transactions (apart from their use as a utility\nwhen writing commands) is that we could build transaction logging.\nSince every StGit command performs exatly one transaction, if we\nsimply logged the before and after values of the patch refs, branch\nref, and patch appliedness, we could build a generic StGit undo/redo\ncommand.\n\n\nExample commands (utillib.py)\n----------------------------------------------------------------------\n\nThis file has sample implementations of some StGit commands: clean,\npop, push, and refresh. They don't have any bells and whistles, and\nthe push is fundamentally limited in that it doesn't handle conflicts\n-- it'll complain and do nothing.\n\nThese were mostly done to excercise the new infrastructure and make\nsure that I hadn't forgotten anything. The plan is not to replace the\nexisting commands, just make them use the new infrastructure.\n\n\n\n\ndiff --git a/stgit/gitlib.py b/stgit/gitlib.py\nnew file mode 100644\nindex 0000000..46911d5\n--- /dev/null\n+++ b/stgit/gitlib.py\n@@ -0,0 +1,360 @@\n+import os, os.path, re\n+from exception import *\n+import run\n+\n+class DetachedHeadException(StgException):\n+    pass\n+\n+class Repr(object):\n+    def __repr__(self):\n+        return str(self)\n+\n+class NoValue(object):\n+    pass\n+\n+def make_defaults(defaults):\n+    def d(val, attr):\n+        if val != NoValue:\n+            return val\n+        elif defaults != NoValue:\n+            return getattr(defaults, attr)\n+        else:\n+            return None\n+    return d\n+\n+class Person(Repr):\n+    \"\"\"Immutable.\"\"\"\n+    def __init__(self, name = NoValue, email = NoValue,\n+                 date = NoValue, defaults = NoValue):\n+        d = make_defaults(defaults)\n+        self.__name = d(name, 'name')\n+        self.__email = d(email, 'email')\n+        self.__date = d(date, 'date')\n+    name = property(lambda self: self.__name)\n+    email = property(lambda self: self.__email)\n+    date = property(lambda self: self.__date)\n+    def set_name(self, name):\n+        return type(self)(name = name, defaults = self)\n+    def set_email(self, email):\n+        return type(self)(email = email, defaults = self)\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+    @classmethod\n+    def parse(cls, s):\n+        m = re.match(r'^([^<]*)<([^>]*)>\\s+(\\d+\\s+[+-]\\d{4})$', s)\n+        assert m\n+        name = m.group(1).strip()\n+        email = m.group(2)\n+        date = m.group(3)\n+        return cls(name, email, date)\n+\n+class Tree(Repr):\n+    \"\"\"Immutable.\"\"\"\n+    def __init__(self, sha1):\n+        self.__sha1 = sha1\n+    sha1 = property(lambda self: self.__sha1)\n+    def __str__(self):\n+        return 'Tree<%s>' % self.sha1\n+\n+class Commitdata(Repr):\n+    \"\"\"Immutable.\"\"\"\n+    def __init__(self, tree = NoValue, parents = NoValue, author = NoValue,\n+                 committer = NoValue, message = NoValue, defaults = NoValue):\n+        d = make_defaults(defaults)\n+        self.__tree = d(tree, 'tree')\n+        self.__parents = d(parents, 'parents')\n+        self.__author = d(author, 'author')\n+        self.__committer = d(committer, 'committer')\n+        self.__message = d(message, 'message')\n+    tree = property(lambda self: self.__tree)\n+    parents = property(lambda self: self.__parents)\n+    @property\n+    def parent(self):\n+        assert len(self.__parents) == 1\n+        return self.__parents[0]\n+    author = property(lambda self: self.__author)\n+    committer = property(lambda self: self.__committer)\n+    message = property(lambda self: self.__message)\n+    def set_tree(self, tree):\n+        return type(self)(tree = tree, defaults = self)\n+    def set_parents(self, parents):\n+        return type(self)(parents = parents, defaults = self)\n+    def add_parent(self, parent):\n+        return type(self)(parents = list(self.parents or []) + [parent],\n+                          defaults = self)\n+    def set_parent(self, parent):\n+        return self.set_parents([parent])\n+    def set_author(self, author):\n+        return type(self)(author = author, defaults = self)\n+    def set_committer(self, committer):\n+        return type(self)(committer = committer, defaults = self)\n+    def set_message(self, message):\n+        return type(self)(message = message, defaults = self)\n+    def __str__(self):\n+        if self.tree == None:\n+            tree = None\n+        else:\n+            tree = self.tree.sha1\n+        if self.parents == None:\n+            parents = None\n+        else:\n+            parents = [p.sha1 for p in self.parents]\n+        return ('Commitdata<tree: %s, parents: %s, author: %s,'\n+                ' committer: %s, message: \"%s\">'\n+                ) % (tree, parents, self.author, self.committer, self.message)\n+    @classmethod\n+    def parse(cls, repository, s):\n+        cd = cls()\n+        lines = list(s.splitlines(True))\n+        for i in xrange(len(lines)):\n+            line = lines[i].strip()\n+            if not line:\n+                return cd.set_message(''.join(lines[i+1:]))\n+            key, value = line.split(None, 1)\n+            if key == 'tree':\n+                cd = cd.set_tree(repository.get_tree(value))\n+            elif key == 'parent':\n+                cd = cd.add_parent(repository.get_commit(value))\n+            elif key == 'author':\n+                cd = cd.set_author(Person.parse(value))\n+            elif key == 'committer':\n+                cd = cd.set_committer(Person.parse(value))\n+            else:\n+                assert False\n+        assert False\n+\n+class Commit(Repr):\n+    \"\"\"Immutable.\"\"\"\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 = Commitdata.parse(\n+                self.__repository,\n+                self.__repository.cat_object(self.sha1))\n+        return self.__data\n+    def __str__(self):\n+        return 'Commit<sha1: %s, data: %s>' % (self.sha1, self.__data)\n+\n+class Refs(object):\n+    def __init__(self, repository):\n+        self.__repository = repository\n+        self.__refs = None\n+    def __cache_refs(self):\n+        self.__refs = {}\n+        for line in self.__repository.run(['git-show-ref']).output_lines():\n+            m = re.match(r'^([0-9a-f]{40})\\s+(\\S+)$', line)\n+            sha1, ref = m.groups()\n+            self.__refs[ref] = sha1\n+    def get(self, ref):\n+        if self.__refs == None:\n+            self.__cache_refs()\n+        return self.__repository.get_commit(self.__refs[ref])\n+    def set(self, ref, commit, msg):\n+        if self.__refs == None:\n+            self.__cache_refs()\n+        old_sha1 = self.__refs.get(ref, '0'*40)\n+        new_sha1 = commit.sha1\n+        if old_sha1 != new_sha1:\n+            self.__repository.run(['git-update-ref', '-m', msg,\n+                                   ref, new_sha1, old_sha1]).no_output()\n+            self.__refs[ref] = new_sha1\n+    def delete(self, ref):\n+        if self.__refs == None:\n+            self.__cache_refs()\n+        self.__repository.run(['git-update-ref',\n+                               '-d', ref, self.__refs[ref]]).no_output()\n+        del self.__refs[ref]\n+\n+class ObjectCache(object):\n+    def __init__(self, create):\n+        self.__objects = {}\n+        self.__create = create\n+    def __getitem__(self, name):\n+        if not name in self.__objects:\n+            self.__objects[name] = self.__create(name)\n+        return self.__objects[name]\n+    def __contains__(self, name):\n+        return name in self.__objects\n+    def __setitem__(self, name, val):\n+        assert not name in self.__objects\n+        self.__objects[name] = val\n+\n+def add_dict(d1, d2):\n+    d = dict(d1)\n+    d.update(d2)\n+    return d\n+\n+class RunWithEnv(object):\n+    def run(self, args, env = {}):\n+        return run.Run(*args).env(add_dict(self.env, env))\n+\n+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.__commits = ObjectCache(lambda sha1: Commit(self, sha1))\n+    env = property(lambda self: { 'GIT_DIR': self.__git_dir })\n+    @classmethod\n+    def default(cls):\n+        \"\"\"Return the default repository.\"\"\"\n+        return cls(run.Run('git-rev-parse', '--git-dir').output_one_line())\n+    def default_index(self):\n+        return Index(self, (os.environ.get('GIT_INDEX_FILE', None)\n+                            or os.path.join(self.__git_dir, 'index')))\n+    def temp_index(self):\n+        return Index(self, self.__git_dir)\n+    def default_worktree(self):\n+        path = os.environ.get('GIT_WORK_TREE', None)\n+        if not path:\n+            o = run.Run('git-rev-parse', '--show-cdup').output_lines()\n+            o = o or ['.']\n+            assert len(o) == 1\n+            path = o[0]\n+        return Worktree(path)\n+    def default_iw(self):\n+        return IndexAndWorktree(self.default_index(), self.default_worktree())\n+    directory = property(lambda self: self.__git_dir)\n+    refs = property(lambda self: self.__refs)\n+    def cat_object(self, sha1):\n+        return self.run(['git-cat-file', '-p', sha1]).raw_output()\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)] = 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+    @property\n+    def head(self):\n+        try:\n+            return self.run(['git-symbolic-ref', '-q', 'HEAD']\n+                            ).output_one_line()\n+        except run.RunException:\n+            raise DetachedHeadException()\n+    def set_head(self, ref, msg):\n+        self.run(['git-symbolic-ref', '-m', msg, 'HEAD', ref]).no_output()\n+    @property\n+    def head_commit(self):\n+        return self.get_commit(self.run(['git-rev-parse', 'HEAD']\n+                                        ).output_one_line())\n+\n+class MergeException(StgException):\n+    pass\n+\n+class Index(RunWithEnv):\n+    def __init__(self, repository, filename):\n+        self.__repository = repository\n+        if os.path.isdir(filename):\n+            # Create a temp index in the given directory.\n+            self.__filename = os.path.join(\n+                filename, 'index.temp-%d-%x' % (os.getpid(), id(self)))\n+            self.delete()\n+        else:\n+            self.__filename = filename\n+    env = property(lambda self: add_dict(self.__repository.env,\n+                                         { 'GIT_INDEX_FILE': self.__filename }))\n+    def read_tree(self, tree):\n+        self.run(['git-read-tree', tree.sha1]).no_output()\n+    def write_tree(self):\n+        return self.__repository.get_tree(\n+            self.run(['git-write-tree']).output_one_line())\n+    def is_clean(self):\n+        try:\n+            self.run(['git-update-index', '--refresh']).discard_output()\n+        except run.RunException:\n+            return False\n+        else:\n+            return True\n+    def merge(self, base, ours, theirs):\n+        \"\"\"In-index merge, no worktree involved.\"\"\"\n+        self.run(['git-read-tree', '-m', '-i', '--aggressive',\n+                  base.sha1, ours.sha1, theirs.sha1]).no_output()\n+        try:\n+            self.run(['git-merge-index', 'git-merge-one-file', '-a']\n+                     ).no_output()\n+        except run.RunException:\n+            raise MergeException('In-index merge failed due to conflicts')\n+    def delete(self):\n+        if os.path.isfile(self.__filename):\n+            os.remove(self.__filename)\n+\n+class Worktree(object):\n+    def __init__(self, directory):\n+        self.__directory = directory\n+    env = property(lambda self: { 'GIT_WORK_TREE': self.__directory })\n+\n+class CheckoutException(StgException):\n+    pass\n+\n+class IndexAndWorktree(RunWithEnv):\n+    def __init__(self, index, worktree):\n+        self.__index = index\n+        self.__worktree = worktree\n+    index = property(lambda self: self.__index)\n+    env = property(lambda self: add_dict(self.__index.env, self.__worktree.env))\n+    def checkout(self, old_commit, new_commit):\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?\n+        try:\n+            self.run(['git-read-tree', '-u', '-m',\n+                      '--exclude-per-directory=.gitignore',\n+                      old_commit.sha1, new_commit.sha1]\n+                     ).discard_output()\n+        except run.RunException:\n+            raise CheckoutException('Index/workdir dirty')\n+    def changed_files(self):\n+        return self.run(['git-diff-files', '--name-only']).output_lines()\n+    def update_index(self, files):\n+        self.run(['git-update-index', '--remove', '-z', '--stdin']\n+                 ).input_nulterm(files).discard_output()\n+\n+if __name__ == '__main__':\n+    testdir = '/tmp/stgtest'\n+    os.system('rm -rf %s' % testdir)\n+    os.makedirs(testdir)\n+    os.chdir(testdir)\n+    for c in ['git init',\n+              'echo foo >> foo',\n+              'git add foo',\n+              'git commit -m foo',\n+              'echo bar >> foo',\n+              'git commit -a -m foo']:\n+        os.system(c)\n+    r = Repository(os.path.join(testdir, '.git'))\n+    head = r.head\n+    c = r.refs.get(head)\n+    print 'HEAD is', head, 'which is', c\n+    c.data\n+    print 'Expanded:', c\n+    maja = Person(name = 'Maja', email = 'maja@example.com')\n+    nisse = Person(name = 'Nisse', email = 'nisse@example.com')\n+    c2 = r.commit(c.data.set_parents([c]).set_author(maja))\n+    c3 = r.commit(c.data.set_parents([c]).set_author(nisse))\n+    c4 = r.commit(c.data.set_parents([c2, c3]))\n+    r.refs.set(head, c4, 'made a cool merge')\n+    c5 = r.commit(c.data.set_parents([c4]).set_tree(\n+        c.data.parents[0].data.tree))\n+    head = 'refs/heads/foobar'\n+    r.refs.set(head, c5, 'committed a revert')\n+    r.set_head(head, 'switched to other branch')\ndiff --git a/stgit/run.py b/stgit/run.py\nindex 924e59a..43c3a23 100644\n--- a/stgit/run.py\n+++ b/stgit/run.py\n@@ -105,7 +105,7 @@ class Run:\n     def input_lines(self, lines):\n         self.__indata = ''.join(['%s\\n' % line for line in lines])\n         return self\n-    def input_nulterm(self, items):\n+    def input_nulterm(self, lines):\n         self.__indata = ''.join('%s\\0' % line for line in lines)\n         return self\n     def no_output(self):\ndiff --git a/stgit/stacklib.py b/stgit/stacklib.py\nnew file mode 100644\nindex 0000000..06ba007\n--- /dev/null\n+++ b/stgit/stacklib.py\n@@ -0,0 +1,120 @@\n+import os.path\n+import gitlib, utils\n+\n+class Patch(object):\n+    def __init__(self, stack, name):\n+        self.__stack = stack\n+        self.__name = name\n+    name = property(lambda self: self.__name)\n+    def __ref(self):\n+        return 'refs/patches/%s/%s' % (self.__stack.name, self.__name)\n+    @property\n+    def commit(self):\n+        return self.__stack.repository.refs.get(self.__ref())\n+    def set_commit(self, commit, msg):\n+        self.__stack.repository.refs.set(self.__ref(), commit, msg)\n+    def delete(self):\n+        self.__stack.repository.refs.delete(self.__ref())\n+    def is_applied(self):\n+        return self.name in self.__stack.patchorder.applied\n+    def is_empty(self):\n+        c = self.commit\n+        return c.data.tree == c.data.parent.data.tree\n+\n+class PatchOrder(object):\n+    \"\"\"Keeps track of patch order, and which patches are applied.\n+    Works with patch names, not actual patches.\"\"\"\n+    __list_order = [ 'applied', 'unapplied' ]\n+    def __init__(self, stack):\n+        self.__stack = stack\n+        self.__lists = {}\n+    def __read_file(self, fn):\n+        return tuple(utils.read_strings(\n+            os.path.join(self.__stack.directory, fn)))\n+    def __write_file(self, fn, val):\n+        utils.write_strings(os.path.join(self.__stack.directory, fn), val)\n+    def __get_list(self, name):\n+        if not name in self.__lists:\n+            self.__lists[name] = self.__read_file(name)\n+        return self.__lists[name]\n+    def __set_list(self, name, val):\n+        val = tuple(val)\n+        if val != self.__lists.get(name, None):\n+            self.__lists[name] = val\n+            self.__write_file(name, val)\n+    applied = property(lambda self: self.__get_list('applied'),\n+                       lambda self, val: self.__set_list('applied', val))\n+    unapplied = property(lambda self: self.__get_list('unapplied'),\n+                         lambda self, val: self.__set_list('unapplied', val))\n+\n+class Patches(object):\n+    \"\"\"Creates Patch objects.\"\"\"\n+    def __init__(self, stack):\n+        self.__stack = stack\n+        def create_patch(name):\n+            p = Patch(self.__stack, name)\n+            p.commit # raise exception if the patch doesn't exist\n+            return p\n+        self.__patches = gitlib.ObjectCache(create_patch) # name -> Patch\n+    def exists(self, name):\n+        return name in self.__patches\n+    def get(self, name):\n+        return self.__patches[name]\n+    def new(self, name, commit, msg):\n+        assert not name in self.__patches\n+        p = Patch(self.__stack, name)\n+        p.set_commit(commit, msg)\n+        self.__patches[name] = p\n+        return p\n+\n+class Stack(object):\n+    def __init__(self, repository, name):\n+        self.__repository = repository\n+        self.__name = name\n+        self.__patchorder = PatchOrder(self)\n+        self.__patches = Patches(self)\n+    name = property(lambda self: self.__name)\n+    repository = property(lambda self: self.__repository)\n+    patchorder = property(lambda self: self.__patchorder)\n+    patches = property(lambda self: self.__patches)\n+    @property\n+    def directory(self):\n+        return os.path.join(self.__repository.directory, 'patches', self.__name)\n+    def __ref(self):\n+        return 'refs/heads/%s' % self.__name\n+    @property\n+    def head(self):\n+        return self.__repository.refs.get(self.__ref())\n+    def set_head(self, commit, msg):\n+        self.__repository.refs.set(self.__ref(), commit, msg)\n+    @property\n+    def base(self):\n+        if self.patchorder.applied:\n+            return self.patches.get(self.patchorder.applied[0]\n+                                    ).commit.data.parent\n+        else:\n+            return self.head\n+\n+def strip_leading(prefix, s):\n+    assert s.startswith(prefix)\n+    return s[len(prefix):]\n+\n+class Repository(gitlib.Repository):\n+    def __init__(self, *args, **kwargs):\n+        gitlib.Repository.__init__(self, *args, **kwargs)\n+        self.__stacks = {} # name -> Stack\n+    @property\n+    def current_branch(self):\n+        return strip_leading('refs/heads/', self.head)\n+    @property\n+    def current_stack(self):\n+        return self.get_stack(self.current_branch)\n+    def get_stack(self, name):\n+        if not name in self.__stacks:\n+            if name == None:\n+                s = None # detached HEAD\n+            else:\n+                # TODO: verify that the branch exists\n+                s = Stack(self, name)\n+            self.__stacks[name] = s\n+        return self.__stacks[name]\ndiff --git a/stgit/translib.py b/stgit/translib.py\nnew file mode 100644\nindex 0000000..deb3420\n--- /dev/null\n+++ b/stgit/translib.py\n@@ -0,0 +1,70 @@\n+import gitlib\n+from exception import *\n+from out import *\n+\n+class TransactionException(StgException):\n+    pass\n+\n+class StackTransaction(object):\n+    def __init__(self, stack, msg):\n+        self.__stack = stack\n+        self.__msg = msg\n+        self.__patches = {}\n+        self.__applied = list(self.__stack.patchorder.applied)\n+        self.__unapplied = list(self.__stack.patchorder.unapplied)\n+    def __set_patches(self, val):\n+        self.__patches = dict(val)\n+    patches = property(lambda self: self.__patches, __set_patches)\n+    def __set_applied(self, val):\n+        self.__applied = list(val)\n+    applied = property(lambda self: self.__applied, __set_applied)\n+    def __set_unapplied(self, val):\n+        self.__unapplied = list(val)\n+    unapplied = property(lambda self: self.__unapplied, __set_unapplied)\n+    def __check_consistency(self):\n+        remaining = set(self.__applied + self.__unapplied)\n+        for pn, commit in self.__patches.iteritems():\n+            if commit == None:\n+                assert self.__stack.patches.exists(pn)\n+            else:\n+                assert pn in remaining\n+    def run(self, iw = None):\n+        self.__check_consistency()\n+\n+        # Get new head commit.\n+        if self.__applied:\n+            top_patch = self.__applied[-1]\n+            try:\n+                new_head = self.__patches[top_patch]\n+            except KeyError:\n+                new_head = self.__stack.patches.get(top_patch).commit\n+        else:\n+            new_head = self.__stack.base\n+\n+        # Set branch head.\n+        if new_head == self.__stack.head:\n+            out.info('Head remains at %s' % new_head.sha1[:8])\n+        elif new_head.data.tree == self.__stack.head.data.tree:\n+            out.info('Head %s -> %s (same tree)' % (self.__stack.head.sha1[:8],\n+                                                    new_head.sha1[:8]))\n+        elif iw != None:\n+            try:\n+                iw.checkout(self.__stack.head, new_head)\n+            except gitlib.CheckoutException, e:\n+                raise TransactionException(e)\n+            out.info('Head %s -> %s' % (self.__stack.head.sha1[:8],\n+                                        new_head.sha1[:8]))\n+        self.__stack.set_head(new_head, self.__msg)\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+                else:\n+                    p.set_commit(commit, self.__msg)\n+            else:\n+                self.__stack.patches.new(pn, commit, self.__msg)\n+        self.__stack.patchorder.applied = self.__applied\n+        self.__stack.patchorder.unapplied = self.__unapplied\ndiff --git a/stgit/utillib.py b/stgit/utillib.py\nnew file mode 100644\nindex 0000000..d09ecc4\n--- /dev/null\n+++ b/stgit/utillib.py\n@@ -0,0 +1,139 @@\n+import gitlib, translib\n+from out import *\n+\n+def head_top_equal(repository):\n+    head = repository.head_commit\n+    try:\n+        s = repository.current_stack\n+    except gitlib.DetachedHeadException:\n+        out.error('Not on any branch (detached HEAD)')\n+        return False\n+    applied = s.patchorder.applied\n+    if not applied:\n+        return True\n+    top = s.patches.get(applied[-1])\n+    if top.commit == head:\n+        return True\n+    out.error('The top patch (%s, %s)' % (top.name, top.commit.sha1),\n+              'and HEAD (%s) are not the same.' % head.sha1)\n+    return False\n+\n+def simple_merge(repository, base, ours, theirs):\n+    \"\"\"Given three trees, tries to do an in-index merge in a temporary\n+    index with a temporary index. Returns the result tree, or None if\n+    the merge failed (due to conflicts).\"\"\"\n+    assert isinstance(base, gitlib.Tree)\n+    assert isinstance(ours, gitlib.Tree)\n+    assert isinstance(theirs, gitlib.Tree)\n+    if base == ours:\n+        # Fast forward: theirs is a descendant of ours.\n+        return theirs\n+    if base == theirs:\n+        # Fast forward: ours is a descendant of theirs.\n+        return ours\n+    index = repository.temp_index()\n+    try:\n+        try:\n+            index.merge(base, ours, theirs)\n+        except gitlib.MergeException:\n+            return None\n+        return index.write_tree()\n+    finally:\n+        index.delete()\n+\n+def clean(stack):\n+    t = translib.StackTransaction(stack, 'stg clean')\n+    t.unapplied = []\n+    for pn in stack.patchorder.unapplied:\n+        p = stack.patches.get(pn)\n+        if p.is_empty():\n+            t.patches[pn] = None\n+        else:\n+            t.unapplied.append[pn]\n+    t.applied = []\n+    parent = stack.base\n+    for pn in stack.patchorder.applied:\n+        p = stack.patches.get(pn)\n+        if p.is_empty():\n+            t.patches[pn] = None\n+            out.info('Deleting %s' % pn)\n+        else:\n+            if parent != p.commit.data.parent:\n+                parent = t.patches[pn] = stack.repository.commit(\n+                    p.commit.data.set_parent(parent))\n+            else:\n+                parent = p.commit\n+            t.applied.append(pn)\n+            out.info('Keeping %s' % pn)\n+    t.run()\n+\n+def pop(stack, iw = None):\n+    t = translib.StackTransaction(stack, 'stg pop')\n+    pn = t.applied.pop()\n+    t.unapplied.insert(0, pn)\n+    t.run(iw)\n+\n+def push(stack, pn, iw = None):\n+    t = translib.StackTransaction(stack, 'stg push')\n+    t.unapplied.remove(pn)\n+    t.applied.append(pn)\n+    p = stack.patches.get(pn)\n+    if stack.head != p.commit.data.parent:\n+        tree = simple_merge(stack.repository, p.commit.data.parent.data.tree,\n+                            stack.head.data.tree, p.commit.data.tree)\n+        assert tree\n+        t.patches[pn] = stack.repository.commit(\n+            p.commit.data.set_parent(stack.head).set_tree(tree))\n+    t.run(iw)\n+\n+def refresh(stack, iw):\n+    iw.update_index(iw.changed_files())\n+    tree = iw.index.write_tree()\n+    t = translib.StackTransaction(stack, 'stg refresh')\n+    p = stack.patches.get(t.applied[-1])\n+    t.patches[p.name] = stack.repository.commit(\n+        p.commit.data.set_tree(tree))\n+    t.run()\n+\n+if __name__ == '__main__':\n+    import os\n+    import stacklib\n+    testdir = '/tmp/stgtest'\n+    os.system('rm -rf %s' % testdir)\n+    os.makedirs(testdir)\n+    os.chdir(testdir)\n+    for c in ['git init',\n+              'echo foo >> foo',\n+              'git add foo',\n+              'git commit -m foo',\n+              'stg init']:\n+        os.system(c)\n+    for i in range(3):\n+        for c in ['stg new p%d -m p%d' % (i, i),\n+                  'echo %s >> foo%d' % (str(i)*4, i),\n+                  'git add foo%d' % i,\n+                  'stg refresh',\n+                  'stg new q%d -m q%d' % (i, i)]:\n+            os.system(c)\n+    r = stacklib.Repository.default()\n+    print 'Current branch:', r.current_branch\n+    s = r.current_stack\n+    print 'Applied:', s.patchorder.applied\n+    print 'Unapplied:', s.patchorder.unapplied\n+    os.system('git checkout HEAD^')\n+    head_top_equal(r)\n+    os.system('git checkout master')\n+    head_top_equal(r)\n+    clean(s)\n+    iw = r.default_iw()\n+    pop(s, iw)\n+    pop(s, iw)\n+    os.system('stg series')\n+    os.system('stg status')\n+    push(s, 'p2', iw)\n+    os.system('stg series')\n+    os.system('stg status')\n+    os.system('echo 333 >> foo0')\n+    refresh(s, iw)\n+    os.system('stg series')\n+    os.system('stg status')\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"58093","messageId":"b0943d9e0711030356j4dcd31cbl54d838107240b3d0@mail.gmail.com","threadId":"10477","inReplyTo":"20071026192418.GA19774@diana.vm.bytemark.co.uk","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2007-11-03T10:56:36Z","receivedAt":"2007-11-03T10:56:36Z","isPatch":false,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"On 26/10/2007, Karl Hasselström <kha@treskal.com> wrote:\n> I wanted to build an StGit command that coalesced adjacent patches to\n> a single patch. Because the end result tree would still be the same,\n> this should be doable without ever involving HEAD, the index, or the\n> worktree.\n\nWouldn't HEAD need to be modified since the commit log changes\nslightly, even though the tree is the same. Or am I misunderstanding\nthis?\n\n> StGit's existing infrastructure for manipulating patches\n> didn't lend itself to doing this kind of thing, though: it's not\n> modular enough. So I started to design a replacement low-level\n> interface to git, and things got slightly out of hand ... and I ended\n> up with a much bigger refactoring than I'd planned.\n\nThanks for this. I'll need a bit of time to read it all and give\nfeedback. In general, I welcome this refactoring.\n\nI'll go through the whole e-mail in the next days and get back to you.\n\n-- \nCatalin\n"},{"id":"58140","messageId":"20071103142851.GG26436@nan92-1-81-57-214-146.fbx.proxad.net","threadId":"10477","inReplyTo":"b0943d9e0711030356j4dcd31cbl54d838107240b3d0@mail.gmail.com","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Yann Dirson","fromEmail":"ydirson@altern.org","sentAt":"2007-11-03T14:28:51Z","receivedAt":"2007-11-03T14:28:51Z","isPatch":false,"sender":{"key":"ydirson@altern.org","avatar":"https://avatars.githubusercontent.com/u/1190950?v=4"},"body":"On Sat, Nov 03, 2007 at 10:56:36AM +0000, Catalin Marinas wrote:\n> On 26/10/2007, Karl Hasselström <kha@treskal.com> wrote:\n> > I wanted to build an StGit command that coalesced adjacent patches to\n> > a single patch. Because the end result tree would still be the same,\n> > this should be doable without ever involving HEAD, the index, or the\n> > worktree.\n> \n> Wouldn't HEAD need to be modified since the commit log changes\n> slightly, even though the tree is the same. Or am I misunderstanding\n> this?\n\nThis reminds me of someone suggesting that some patches could be\nrepresented by more than one commit.  But I'm not sure such a beast\nwould be useful - I fear that would make StGIT much more complicated,\nbut would it really make things better ?\n\nBest regards,\n-- \nYann\n"},{"id":"58290","messageId":"20071104183457.GA6032@diana.vm.bytemark.co.uk","threadId":"10477","inReplyTo":"b0943d9e0711030356j4dcd31cbl54d838107240b3d0@mail.gmail.com","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-11-04T18:34:57Z","receivedAt":"2007-11-04T18:34:57Z","isPatch":false,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2007-11-03 10:56:36 +0000, Catalin Marinas wrote:\n\n> On 26/10/2007, Karl Hasselström <kha@treskal.com> wrote:\n>\n> > I wanted to build an StGit command that coalesced adjacent patches\n> > to a single patch. Because the end result tree would still be the\n> > same, this should be doable without ever involving HEAD, the\n> > index, or the worktree.\n>\n> Wouldn't HEAD need to be modified since the commit log changes\n> slightly, even though the tree is the same. Or am I misunderstanding\n> this?\n\nI'm refering to the HEAD tree. The HEAD commit will of course change.\n\n> > StGit's existing infrastructure for manipulating patches didn't\n> > lend itself to doing this kind of thing, though: it's not modular\n> > enough. So I started to design a replacement low-level interface\n> > to git, and things got slightly out of hand ... and I ended up\n> > with a much bigger refactoring than I'd planned.\n>\n> Thanks for this. I'll need a bit of time to read it all and give\n> feedback. In general, I welcome this refactoring.\n>\n> I'll go through the whole e-mail in the next days and get back to\n> you.\n\nThanks, I appreciate it.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"58291","messageId":"20071104184003.GB6032@diana.vm.bytemark.co.uk","threadId":"10477","inReplyTo":"20071103142851.GG26436@nan92-1-81-57-214-146.fbx.proxad.net","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-11-04T18:40:03Z","receivedAt":"2007-11-04T18:40:03Z","isPatch":false,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2007-11-03 15:28:51 +0100, Yann Dirson wrote:\n\n> This reminds me of someone suggesting that some patches could be\n> represented by more than one commit.\n\nYou might be remebering me pointing out that the old infrastructure\nsupported (or at least not directly disallowed) this.\n\n> But I'm not sure such a beast would be useful - I fear that would\n> make StGIT much more complicated, but would it really make things\n> better?\n\nYes, it makes everything much more complicated, and no, it doesn't buy\nus anything new. After all, once we know the parent and the tree we\nwant a patch to have, we can just manufacture a commit that has that\ntree and that parent.\n\nMy proposed new infrastructure cannot represent such patches, very\nmuch by design.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"60009","messageId":"b0943d9e0711151028y22b37e04y87a6bc74dc4c5031@mail.gmail.com","threadId":"10477","inReplyTo":"20071026192418.GA19774@diana.vm.bytemark.co.uk","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Catalin Marinas","fromEmail":"catalin.marinas@gmail.com","sentAt":"2007-11-15T18:28:16Z","receivedAt":"2007-11-15T18:28:16Z","isPatch":false,"sender":{"key":"catalin.marinas@gmail.com","avatar":null},"body":"On 26/10/2007, Karl Hasselström <kha@treskal.com> wrote:\n> I wanted to build an StGit command that coalesced adjacent patches to\n> a single patch. Because the end result tree would still be the same,\n> this should be doable without ever involving HEAD, the index, or the\n> worktree. StGit's existing infrastructure for manipulating patches\n> didn't lend itself to doing this kind of thing, though: it's not\n> modular enough. So I started to design a replacement low-level\n> interface to git, and things got slightly out of hand ... and I ended\n> up with a much bigger refactoring than I'd planned.\n\nI eventually managed to have a (not so in-depth) look at this and I am\nOK with it (but merging after 0.14). As you said the current structure\nand the new one will work in parallel for some time. It would even be\npossible to implement unit testing.\n\nSome random comments below:\n\nCommitdata maybe should be written as CommitData (as in the e-mail text).\n\nThe Repository.head property is not cached and it is probably OK to\navoid some problems I had. We could run profiling afterwards to see\nhow often it is called.\n\n> Transactions (translib.py)\n> ----------------------------------------------------------------------\n>\n> I started to implement a few StGit commands on top of gitlib.py and\n> stacklib.py, and then realized something very appealing:\n>\n>   Just about every StGit command can be accomplished by first creating\n>   a bunch of new commit objects with gitlib.py, then trying to check\n>   out the new HEAD, and then rewriting refs with stacklib.py. Only the\n>   first and second steps can possibly fail, and if they do, they do so\n>   without leaving any user-visible junk behind. This can be used to\n>   make all commands either succeed completely, or do nothing at all.\n\nAs you said, that's a bit difficult for 'push' as it is made of a\nseries of separate individual patch pushes. Can we not use a temporary\nindex by setting GIT_INDEX_FILE during the whole transaction and\nfreely update the working copy. Only if the transaction fails, we\nclean the working copy and check out the default index? This might\nslow down some operations though.\n\nIn the future, it would be nice to record the stack state before\ntransactions in a git object (via pickle) together with HEAD id and\nprovide unlimited undo.\n\nThanks.\n\n-- \nCatalin\n"},{"id":"60063","messageId":"20071116074255.GA16975@diana.vm.bytemark.co.uk","threadId":"10477","inReplyTo":"b0943d9e0711151028y22b37e04y87a6bc74dc4c5031@mail.gmail.com","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-11-16T07:42:55Z","receivedAt":"2007-11-16T07:42:55Z","isPatch":false,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2007-11-15 18:28:16 +0000, Catalin Marinas wrote:\n\n> I eventually managed to have a (not so in-depth) look at this\n\nThanks!\n\n> and I am OK with it (but merging after 0.14).\n\nYes, that seems like a good idea.\n\n> As you said the current structure and the new one will work in\n> parallel for some time.\n\nYes. This is a very nice property that makes the whole enterprise\nfeasible.\n\n> It would even be possible to implement unit testing.\n\nYes. Unit testing would be easier with the new code, since it's more\nmodular.\n\n> Commitdata maybe should be written as CommitData (as in the e-mail\n> text).\n\nProbably. I've alredy made that spelling error myself at least once\nwhen writing code.\n\n> The Repository.head property is not cached and it is probably OK to\n> avoid some problems I had. We could run profiling afterwards to see\n> how often it is called.\n\nYes. This applies to a few other things as well -- I try to be careful\nto write things so that they could be cached transparently if it turns\nout we need it. But I also try to not optimize prematurely.\n\nIn some cases the caching is more than just a speed optimization.\nCommits and trees can be compared with regular Python object identity,\nfor example, because I never create duplicates.\n\n> > I started to implement a few StGit commands on top of gitlib.py\n> > and stacklib.py, and then realized something very appealing:\n> >\n> >   Just about every StGit command can be accomplished by first\n> >   creating a bunch of new commit objects with gitlib.py, then\n> >   trying to check out the new HEAD, and then rewriting refs with\n> >   stacklib.py. Only the first and second steps can possibly fail,\n> >   and if they do, they do so without leaving any user-visible junk\n> >   behind. This can be used to make all commands either succeed\n> >   completely, or do nothing at all.\n>\n> As you said, that's a bit difficult for 'push' as it is made of a\n> series of separate individual patch pushes. Can we not use a\n> temporary index by setting GIT_INDEX_FILE during the whole\n> transaction and freely update the working copy. Only if the\n> transaction fails, we clean the working copy and check out the\n> default index? This might slow down some operations though.\n\nThe sample code I posted does exactly this index trick. So yes, I\nthink it's a splendid idea. :-)\n\nBasically, what it does (or eventually should do) is repeatedly use\nthe temp index to create the new commits without ever touching the\ndefault index or the worktree. Only if a merge conflicts would we\ncheck out one of the parents and spill the conflicts to the worktree.\n(And once we're finished pushing we'd want to check out the new stack\ntop, obviously.)\n\nThe only problem is that while git-read-tree can do a three-way merge\nin a temp index without touching the worktree, git-merge-recursive\nassumes that the worktree reflects one of the trees to be merged and\ncan be written to. Which is _totally OK_ if there really is going to\nbe a conflict. But merge-recursive does fancier conflict resolution\nthan read-tree (I think -- I still have to look up exactly what, but I\ndon't think read-tree does rename detection for example), so it might\nsucceed even if read-tree failed.\n\n> In the future, it would be nice to record the stack state before\n> transactions in a git object (via pickle) together with HEAD id and\n> provide unlimited undo.\n\nYes, this was exactly what I was refering to when I talked about\ntransaction logging. The only difference is that I'd prefer a simple\ntext format instead of a pickle, but we can have a fight about that\ndetail later. :-)\n\nThe complete state that would have to be saved would be\n\n  * (patchname, commit sha1) pairs for all patches\n\n  * (branchname, branch head sha1) pair\n\n  * the ordered lists of applied, unapplied, and eventually also\n    hidden patches\n\nThis is somewhat redundant (applied/unapplied could be inferred from\nthe commit DAG, etc.) but it would allow us to represent relevant\nillegal states as well, which is needed in order to be able to undo\nrepairs of inconsistent stacks.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"},{"id":"60066","messageId":"20071116083621.GA18039@diana.vm.bytemark.co.uk","threadId":"10477","inReplyTo":"20071116074255.GA16975@diana.vm.bytemark.co.uk","subject":"Re: [StGit RFC] A more structured way of calling git","fromName":"Karl Hasselström","fromEmail":"kha@treskal.com","sentAt":"2007-11-16T08:36:21Z","receivedAt":"2007-11-16T08:36:21Z","isPatch":false,"sender":{"key":"kha@treskal.com","avatar":"https://gravatar.com/avatar/f0120c734b5279b345075a28521e1ac66acb20c9913ffe9bf6ae97e53f7f3f13?d=mp&s=160"},"body":"On 2007-11-16 08:42:55 +0100, Karl Hasselström wrote:\n\n> The complete state that would have to be saved would be\n>\n>   * (patchname, commit sha1) pairs for all patches\n>\n>   * (branchname, branch head sha1) pair\n>\n>   * the ordered lists of applied, unapplied, and eventually also\n>     hidden patches\n\nOh, and\n\n  * .git/config (one blob)\n\n  * the worktree (one tree object?)\n\n  * the index (one tree object per stage?)\n\nOtherwise, we won't be able to un-clobber them. But a first\nimplementation could certainly skip this part.\n\n-- \nKarl Hasselström, kha@treskal.com\n      www.treskal.com/kalle\n"}]}