<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>.gitignore</filename>
    </added>
    <added>
      <filename>gitshelve.py</filename>
    </added>
    <added>
      <filename>t_gitshelve.py</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -773,6 +773,186 @@ def git(cmd, *args, **kwargs):
     if not kwargs.has_key('ignore_output'):
         return proc.stdout.read()[:-1]
 
+class gitbook:
+    &quot;&quot;&quot;Abstracts a reference to a data file within a Git repository.  It also
+    maintains knowledge of whether the object has been modified or not.&quot;&quot;&quot;
+    def __init__(self, shelf, path, hash = None):
+        self.shelf = shelf
+        self.path  = path
+        self.hash  = hash
+        self.data  = None
+        self.dirty = False
+
+    def data(self):
+        if self.data is None:
+            assert self.hash is not None
+            self.data = git('cat-file', 'blob', self.hash)
+        return self.data
+        
+    def set_data(self, data):
+        self.hash = None
+        self.data = data
+        self.mark_dirty()
+        
+    def mark_dirty(self):
+        self.dirty = True
+
+    def __getstate__(self):
+        odict = self.__dict__.copy() # copy the dict since we change it
+        del odict['dirty']           # remove dirty flag
+        return odict
+
+    def __setstate__(self,dict):
+        self.__dict__.update(dict)   # update attributes
+        self.dirty = False
+
+
+class gitshelve(dict):
+    &quot;&quot;&quot;This class implements a Python &quot;shelf&quot; using a branch within a Git
+    repository.  There is no &quot;writeback&quot; argument, meaning changes are only
+    written upon calling close or sync.
+
+    This implementation uses a dictionary of gitbook objects, since we don't
+    really want to use Pickling within a Git repository (it's not friendly to
+    other Git users, nor does it support merging).&quot;&quot;&quot;
+    ls_tree_pat = re.compile('(040000 tree|100644 blob) ([0-9a-f]{40})\t(start|(.+))$')
+
+    def __init__(self, branch):
+        self.dirty      = False
+        self.branch     = branch
+        self.trees      = {}
+        self.paths      = {}
+
+    def create_branch(self, cmd, args, kwargs):
+        &quot;&quot;&quot;If an issues branch already exists at the remote, we simply refer
+        to it from now on.  Otherwise, we create a dummy commit in order get
+        things started.&quot;&quot;&quot;
+        try:
+            hash = git('rev-parse', 'origin/%s' % self.branch,
+                       no_stderr = True)
+        except:
+            msg  = &quot;Created %s branch\n&quot; % self.branch
+            hash = git('hash-object', '-w', '--stdin', input = msg)
+            hash = git('mktree', input = &quot;100644 blob %s\tstart\n&quot; % hash)
+            hash = git('commit-tree', hash, input = msg)
+
+        git('branch', self.branch, hash)
+        return True
+
+    def current_head(self):
+        return git('rev-parse', self.branch, restart = self.create_branch)
+
+    def open(cls, branch):
+        shelf = gitshelve(branch)
+        
+        ls_tree = string.split(git('ls-tree', '-r', '-t', '-z',
+                                   self.current_head()), '\0')
+        for line in ls_tree:
+            match = cls.ls_tree_pat.match(line)
+            assert match
+
+            hash = match.group(2)
+            path = match.group(3)
+
+            if match.group(1) == &quot;040000 tree&quot;:
+                parts = os.path.split(path)
+
+                dict = shelf.trees
+
+                for part in parts:
+                    if not dict.has_key(part):
+                        dict[part] = {}
+                    dict = dict[part]
+
+                dict['__root__'] = hash
+
+            elif path == 'start':
+                break
+
+            else:
+                assert not shelf.paths.has_key(path)
+                shelf.paths[path] = gitbook(shelf, path, hash)
+
+        return shelf
+
+    open = classmethod(open)
+
+    def sync(self):
+        if self.dirty:
+            for book in self.paths:
+                if book.dirty:
+                    self.sync_book(book)
+        self.dirty = False
+
+    def sync_book(self, book):
+        # Create a unique blob to represent the new issue.  This is the
+        # issue's official object name from now on, and will never change.
+        blob = git('hash-object', '-w', '--stdin', input = repr(book))
+        book.hash  = blob
+        book.dirty = False
+
+        tree = git('mktree', input = &quot;100644 blob %s\tinfo\n&quot; % blob)
+
+        # Merge this blob with existing issue blobs that share the same
+        # first two hash digits
+        if self.trees.has_key(blob[:2]):
+            parent = self.trees[blob[:2]]
+        else:
+            parent = [None]
+            self.trees[blob[:2]] = parent
+        parent.append((tree, blob[2:]))
+
+        buffer = StringIO()
+        for child in parent[1:]:
+            buffer.write(&quot;040000 tree %s\t%s\n&quot; % (child[0], child[1]))
+
+        tree = git('mktree', input = buffer.getvalue())
+
+        self.trees[blob[:2]][0] = tree
+
+        # Merge it into the tree of issues overall
+        keys = self.trees.keys()
+        keys.sort()
+
+        buffer = StringIO()
+        for key in keys:
+            buffer.write(&quot;040000 tree %s\t%s\n&quot; % (self.trees[key][0], key))
+
+        tree = git('mktree', input = buffer.getvalue())
+
+        # Commit the merged tree (though at this moment it's a dangling commit)
+        commit = git('commit-tree', tree, '-p', self.current_head(),
+                     input = book.title)
+
+        # Update the head of the issues branch to point to the new commit
+        self.update_head(commit)
+
+    def close(self):
+        if self.dirty:
+            self.sync()
+        del self.objects        # free it up right away
+
+    def __getitem__(self, path):
+        return self.paths[path].data()
+
+    def __setitem__(self, path, data):
+        if not self.paths.has_key(path):
+            self.paths[path] = gitbook(self, path)
+        self.paths[path].set_data(data)
+        self.dirty = True
+
+    def __detitem__(self, path):
+        del self.paths[path]
+
+    def __contains__(self, path):
+        return self.paths.has_key()
+
+    def __iter__(self):
+        pass
+
+    def iteritems(self):
+        pass
+
 class GitIssueSet(IssueSet):
     &quot;&quot;&quot;This object implements all the command necessary to interact with Git
     for the purpose of storing and distributing issues.&quot;&quot;&quot;
@@ -941,129 +1121,131 @@ def get_issue(issueSet, id):
 
 ######################################################################
 
-if len(args) == 0:
-    print &quot;Show help here.&quot;
-    sys.exit(1)
+if __name__ == '__main__':
 
-command = args[0]
-args    = args[1:]
+    if len(args) == 0:
+        print &quot;Show help here.&quot;
+        sys.exit(1)
+
+    command = args[0]
+    args    = args[1:]
 
 ######################################################################
 
-# jww (2008-05-12): Pick the appropriate IssueSet to used based on what we
-# find in our environment.
+    # jww (2008-05-12): Pick the appropriate IssueSet to used based on what we
+    # find in our environment.
 
-issueSet = IssueSet.load_state(GitIssueSet())
+    issueSet = IssueSet.load_state(GitIssueSet())
 
 ######################################################################
 
-if command == &quot;list&quot;:
-    print
-    print &quot;   #    Id     Title                   State  Date  Assign  Tags&quot;
-    print &quot;-------------------------------------------------------------------------------&quot;
+    if command == &quot;list&quot;:
+        print
+        print &quot;   #    Id     Title                   State  Date  Assign  Tags&quot;
+        print &quot;-------------------------------------------------------------------------------&quot;
 
-    index = 1
-    for name in issueSet.issues.keys():
-        issue = issueSet.issues[name]
+        index = 1
+        for name in issueSet.issues.keys():
+            issue = issueSet.issues[name]
 
-        print &quot;%4d  %s  %-23s %-6s %5s %6s %s&quot; % \
-            (index, issue.name[:7], issue.title, issue.status,
-             issue.created and issue.created.strftime('%m/%d'),
-             str(issue.author)[:6], '')
+            print &quot;%4d  %s  %-23s %-6s %5s %6s %s&quot; % \
+                (index, issue.name[:7], issue.title, issue.status,
+                 issue.created and issue.created.strftime('%m/%d'),
+                 str(issue.author)[:6], '')
 
-        index += 1
+            index += 1
 
-    print
+        print
 
 ######################################################################
 
-elif command == &quot;show&quot; or command == &quot;dump&quot;:
-    if len(args) == 0:
-        print &quot;Shows needs an index or Id.&quot;
-
-    issue = get_issue(issueSet, args[0])
-    if command == &quot;show&quot;:
-        sys.stdout.write(&quot;&quot;&quot;
-      Title: %s
-    Summary: %s
-
-Description: %s
-
-     Author: %s
-   Reporter: %s
-      Owner: %s
-   Assigned: %s
-         Cc: %s
-
-       Type: %s
-     Status: %s
- Resolution: %s
- Components: %s
-    Version: %s
-  Milestone: %s
-   Severity: %s
-   Priority: %s
-       Tags: %s
-   
-    Created: %s
-   Modified: %s
-
-&quot;&quot;&quot; % (issue.title,
-       issue.format_long_text(issue.summary),
-
-       issue.format_long_text(issue.description),
-
-       issue.author,
-       issue.format_people_list(issue.reporters),
-       issue.format_people_list(issue.owners),
-       issue.format_people_list(issue.assigned),
-       issue.format_people_list(issue.carbons),
-
-       issue.issue_type,
-       issue.status,
-       issue.resolution or &quot;&quot;,
-       issue.components or &quot;&quot;,
-       issue.version or &quot;&quot;,
-       issue.milestone or &quot;&quot;,
-       issue.severity,
-       issue.priority,
-       issue.tags or &quot;&quot;,
-
-       issue.created,
-       issue.modified))
-    else:
-        write_object(issue)
+    elif command == &quot;show&quot; or command == &quot;dump&quot;:
+        if len(args) == 0:
+            print &quot;Shows needs an index or Id.&quot;
+
+        issue = get_issue(issueSet, args[0])
+        if command == &quot;show&quot;:
+            sys.stdout.write(&quot;&quot;&quot;
+          Title: %s
+        Summary: %s
+
+    Description: %s
+
+         Author: %s
+       Reporter: %s
+          Owner: %s
+       Assigned: %s
+             Cc: %s
+
+           Type: %s
+         Status: %s
+     Resolution: %s
+     Components: %s
+        Version: %s
+      Milestone: %s
+       Severity: %s
+       Priority: %s
+           Tags: %s
+
+        Created: %s
+       Modified: %s
+
+    &quot;&quot;&quot; % (issue.title,
+           issue.format_long_text(issue.summary),
+
+           issue.format_long_text(issue.description),
+
+           issue.author,
+           issue.format_people_list(issue.reporters),
+           issue.format_people_list(issue.owners),
+           issue.format_people_list(issue.assigned),
+           issue.format_people_list(issue.carbons),
+
+           issue.issue_type,
+           issue.status,
+           issue.resolution or &quot;&quot;,
+           issue.components or &quot;&quot;,
+           issue.version or &quot;&quot;,
+           issue.milestone or &quot;&quot;,
+           issue.severity,
+           issue.priority,
+           issue.tags or &quot;&quot;,
+
+           issue.created,
+           issue.modified))
+        else:
+            write_object(issue)
 
 ######################################################################
 
-elif command == &quot;change&quot;:
-    if len(args) == 0:
-        print &quot;Change needs an issue.&quot;
+    elif command == &quot;change&quot;:
+        if len(args) == 0:
+            print &quot;Change needs an issue.&quot;
 
-    issue = get_issue(issueSet, args[0])
+        issue = get_issue(issueSet, args[0])
 
-    # jww (2008-05-13): Need to parse datetime, lists, and people
-    eval (&quot;issue.set_%s(\&quot;%s\&quot;)&quot; % (args[1], args[2]))
+        # jww (2008-05-13): Need to parse datetime, lists, and people
+        eval (&quot;issue.set_%s(\&quot;%s\&quot;)&quot; % (args[1], args[2]))
 
 ######################################################################
 
-elif command == &quot;new&quot;:
-    if len(args) == 0:
-        print &quot;New needs a title.&quot;
+    elif command == &quot;new&quot;:
+        if len(args) == 0:
+            print &quot;New needs a title.&quot;
 
-    issue = Issue(issueSet, issueSet.current_author(), args[0])
-    issueSet.add_issue(issue)
-    issue.mark_dirty(True)
+        issue = Issue(issueSet, issueSet.current_author(), args[0])
+        issueSet.add_issue(issue)
+        issue.mark_dirty(True)
 
 ######################################################################
 
-# If any of the commands made the issueSet dirty, (possibly) update the
-# repository and write out a new cache
+    # If any of the commands made the issueSet dirty, (possibly) update the
+    # repository and write out a new cache
 
-issueSet.save_state()
+    issueSet.save_state()
 
 ######################################################################
 
-sys.exit(0)
+    sys.exit(0)
 
 # git-issue ends here</diff>
      <filename>git-issues</filename>
    </modified>
  </modified>
  <removed type="array"/>
  <parents type="array">
    <parent>
      <id>cab44d22ec25ed063f4c772a8cf1903c7b283246</id>
    </parent>
  </parents>
  <author>
    <name>John Wiegley</name>
    <email>johnw@newartisans.com</email>
  </author>
  <url>http://github.com/jwiegley/git-issues/commit/b6d2d87dc2011d19945e4484137ffbd1377f0aed</url>
  <id>b6d2d87dc2011d19945e4484137ffbd1377f0aed</id>
  <committed-date>2008-05-14T18:32:03-07:00</committed-date>
  <authored-date>2008-05-14T18:32:03-07:00</authored-date>
  <message>Created a gitshelve Python object, for easily writing scripts that store
arbitrary data inside a Git repository.</message>
  <tree>38f51ce848206efb63dabd8d86b50705994f78b3</tree>
  <committer>
    <name>John Wiegley</name>
    <email>johnw@newartisans.com</email>
  </committer>
</commit>
