Browse Source

* New upstream release.
+ Supports time-less tags. Closes: #543240
* Rebuilt against newer version of python-central. Closes: #551893

Jelmer Vernooij 15 years ago
parent
commit
757919b726
12 changed files with 517 additions and 191 deletions
  1. 12 0
      NEWS
  2. 129 121
      bin/dulwich
  3. 8 0
      debian/changelog
  4. 1 1
      docs/tutorial/2-change-file.txt
  5. 1 1
      dulwich/__init__.py
  6. 19 0
      dulwich/client.py
  7. 28 1
      dulwich/index.py
  8. 85 7
      dulwich/object_store.py
  9. 58 56
      dulwich/objects.py
  10. 110 0
      dulwich/patch.py
  11. 59 0
      dulwich/repo.py
  12. 7 4
      setup.py

+ 12 - 0
NEWS

@@ -1,3 +1,15 @@
+0.4.1	2010-01-03
+
+ FEATURES
+
+  * Add ObjectStore.iter_tree_contents()
+
+  * Add Index.changes_from_tree()
+
+  * Add ObjectStore.tree_changes()
+
+  * Add functionality for writing patches in dulwich.patch.
+
 0.4.0	2009-10-07
 0.4.0	2009-10-07
 
 
  DOCUMENTATION
  DOCUMENTATION

+ 129 - 121
bin/dulwich

@@ -31,156 +31,164 @@ def get_transport_and_path(uri):
 
 
 
 
 def cmd_fetch_pack(args):
 def cmd_fetch_pack(args):
-	from dulwich.repo import Repo
-	opts, args = getopt(args, "", ["all"])
-	opts = dict(opts)
-        client, path = get_transport_and_path(args.pop(0))
-	if "--all" in opts:
-		determine_wants = r.object_store.determine_wants_all
-	else:
-		determine_wants = lambda x: [y for y in args if not y in r.object_store]
-	r = Repo(".")
-	graphwalker = r.get_graph_walker()
-	f, commit = r.object_store.add_pack()
-	try:
-            client.fetch_pack(path, determine_wants, graphwalker, f.write, sys.stdout.write)
-        finally:
-		commit()
+    from dulwich.repo import Repo
+    opts, args = getopt(args, "", ["all"])
+    opts = dict(opts)
+    client, path = get_transport_and_path(args.pop(0))
+    r = Repo(".")
+    if "--all" in opts:
+        determine_wants = r.object_store.determine_wants_all
+    else:
+        determine_wants = lambda x: [y for y in args if not y in r.object_store]
+    graphwalker = r.get_graph_walker()
+    client.fetch(path, r.object_store, determine_wants)
 
 
 
 
 def cmd_log(args):
 def cmd_log(args):
-	from dulwich.repo import Repo
-	opts, args = getopt(args, "", [])
-	r = Repo(".")
-	todo = [r.head()]
-	done = set()
-	while todo:
-		sha = todo.pop()
-		assert isinstance(sha, str)
-		if sha in done:
-			continue
-		done.add(sha)
-		commit = r.commit(sha)
-		print "-" * 50
-		print "commit: %s" % sha
-		if len(commit.parents) > 1:
-			print "merge: %s" % "...".join(commit.parents[1:])
-		print "author: %s" % commit.author
-		print "committer: %s" % commit.committer
-		print ""
-		print commit.message
-		print ""
-		todo.extend([p for p in commit.parents if p not in done])
+    from dulwich.repo import Repo
+    opts, args = getopt(args, "", [])
+    r = Repo(".")
+    todo = [r.head()]
+    done = set()
+    while todo:
+        sha = todo.pop()
+        assert isinstance(sha, str)
+        if sha in done:
+            continue
+        done.add(sha)
+        commit = r.commit(sha)
+        print "-" * 50
+        print "commit: %s" % sha
+        if len(commit.parents) > 1:
+            print "merge: %s" % "...".join(commit.parents[1:])
+        print "author: %s" % commit.author
+        print "committer: %s" % commit.committer
+        print ""
+        print commit.message
+        print ""
+        todo.extend([p for p in commit.parents if p not in done])
 
 
 
 
 def cmd_dump_pack(args):
 def cmd_dump_pack(args):
-	from dulwich.errors import ApplyDeltaError
-	from dulwich.pack import Pack, sha_to_hex
-	import os
-	import sys
-
-	opts, args = getopt(args, "", [])
-
-	if args == []:
-		print "Usage: dulwich dump-pack FILENAME"
-		sys.exit(1)
-
-	basename, _ = os.path.splitext(args[0])
-	x = Pack(basename)
-	print "Object names checksum: %s" % x.name()
-	print "Checksum: %s" % sha_to_hex(x.get_stored_checksum())
-	if not x.check():
-		print "CHECKSUM DOES NOT MATCH"
-	print "Length: %d" % len(x)
-	for name in x:
-		try:
-			print "\t%s" % x[name]
-		except KeyError, k:
-			print "\t%s: Unable to resolve base %s" % (name, k)
-		except ApplyDeltaError, e:
-			print "\t%s: Unable to apply delta: %r" % (name, e)
+    from dulwich.errors import ApplyDeltaError
+    from dulwich.pack import Pack, sha_to_hex
+    import os
+    import sys
+
+    opts, args = getopt(args, "", [])
+
+    if args == []:
+        print "Usage: dulwich dump-pack FILENAME"
+        sys.exit(1)
+
+    basename, _ = os.path.splitext(args[0])
+    x = Pack(basename)
+    print "Object names checksum: %s" % x.name()
+    print "Checksum: %s" % sha_to_hex(x.get_stored_checksum())
+    if not x.check():
+        print "CHECKSUM DOES NOT MATCH"
+    print "Length: %d" % len(x)
+    for name in x:
+        try:
+            print "\t%s" % x[name]
+        except KeyError, k:
+            print "\t%s: Unable to resolve base %s" % (name, k)
+        except ApplyDeltaError, e:
+            print "\t%s: Unable to apply delta: %r" % (name, e)
 
 
 
 
 def cmd_dump_index(args):
 def cmd_dump_index(args):
-	from dulwich.index import Index
+    from dulwich.index import Index
 
 
-	opts, args = getopt(args, "", [])
+    opts, args = getopt(args, "", [])
 
 
-	if args == []:
-		print "Usage: dulwich dump-index FILENAME"
-		sys.exit(1)
+    if args == []:
+        print "Usage: dulwich dump-index FILENAME"
+        sys.exit(1)
 
 
-	filename = args[0]
-	idx = Index(filename)
+    filename = args[0]
+    idx = Index(filename)
 
 
-	for o in idx:
-		print o, idx[o]
+    for o in idx:
+        print o, idx[o]
 
 
 
 
 def cmd_init(args):
 def cmd_init(args):
-	from dulwich.repo import Repo
-	import os
-	import sys
-	opts, args = getopt(args, "", ["--bare"])
-	opts = dict(opts)
+    from dulwich.repo import Repo
+    import os
+    opts, args = getopt(args, "", ["--bare"])
+    opts = dict(opts)
 
 
-	if args == []:
-		path = os.getcwd()
-	else:
-		path = args[0]
+    if args == []:
+        path = os.getcwd()
+    else:
+        path = args[0]
 
 
-	if not os.path.exists(path):
-		os.mkdir(path)
+    if not os.path.exists(path):
+        os.mkdir(path)
 
 
-	if "--bare" in opts:
-		Repo.init_bare(path)
-	else:
-		Repo.init(path)
+    if "--bare" in opts:
+        Repo.init_bare(path)
+    else:
+        Repo.init(path)
 
 
 
 
 def cmd_clone(args):
 def cmd_clone(args):
-	from dulwich.repo import Repo
-	import os
-	import sys
-	opts, args = getopt(args, "", [])
-	opts = dict(opts)
-
-	if args == []:
-		print "usage: dulwich clone host:path [PATH]"
-		sys.exit(1)
+    from dulwich.repo import Repo
+    import os
+    import sys
+    opts, args = getopt(args, "", [])
+    opts = dict(opts)
+
+    if args == []:
+        print "usage: dulwich clone host:path [PATH]"
+        sys.exit(1)
         client, host_path = get_transport_and_path(args.pop(0))
         client, host_path = get_transport_and_path(args.pop(0))
 
 
-	if len(args) > 0:
-		path = args.pop(0)
-	else:
-		path = host_path.split("/")[-1]
-
-	if not os.path.exists(path):
-		os.mkdir(path)
-	Repo.init(path)
-	r = Repo(path)
-	graphwalker = r.get_graph_walker()
-	f, commit = r.object_store.add_pack()
-	client.fetch_pack(host_path, r.object_store.determine_wants_all, 
-			          graphwalker, f.write, sys.stdout.write)
-	commit()
-
+    if len(args) > 0:
+        path = args.pop(0)
+    else:
+        path = host_path.split("/")[-1]
+
+    if not os.path.exists(path):
+        os.mkdir(path)
+    Repo.init(path)
+    r = Repo(path)
+    graphwalker = r.get_graph_walker()
+    f, commit = r.object_store.add_pack()
+    client.fetch_pack(host_path, r.object_store.determine_wants_all, 
+                      graphwalker, f.write, sys.stdout.write)
+    commit()
+
+
+def cmd_commit(args):
+    from dulwich.repo import Repo
+    import os
+    opts, args = getopt(args, "", ["message"])
+    opts = dict(opts)
+    r = Repo(".")
+    committer = "%s <%s>" % (os.getenv("GIT_COMMITTER_NAME"), 
+                             os.getenv("GIT_COMMITTER_EMAIL"))
+    author = "%s <%s>" % (os.getenv("GIT_AUTHOR_NAME"), 
+                          os.getenv("GIT_AUTHOR_EMAIL"))
+    r.do_commit(committer=committer, author=author, message=opts["--message"])
 
 
 commands = {
 commands = {
-	"fetch-pack": cmd_fetch_pack,
-	"dump-pack": cmd_dump_pack,
-	"dump-index": cmd_dump_index,
-	"init": cmd_init,
-	"log": cmd_log,
-	"clone": cmd_clone,
-	}
+    "commit": cmd_commit,
+    "fetch-pack": cmd_fetch_pack,
+    "dump-pack": cmd_dump_pack,
+    "dump-index": cmd_dump_index,
+    "init": cmd_init,
+    "log": cmd_log,
+    "clone": cmd_clone,
+    }
 
 
 if len(sys.argv) < 2:
 if len(sys.argv) < 2:
-	print "Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys()))
-	sys.exit(1)
+    print "Usage: %s <%s> [OPTIONS...]" % (sys.argv[0], "|".join(commands.keys()))
+    sys.exit(1)
 
 
 cmd = sys.argv[1]
 cmd = sys.argv[1]
 if not cmd in commands:
 if not cmd in commands:
-	print "No such subcommand: %s" % cmd
-	sys.exit(1)
+    print "No such subcommand: %s" % cmd
+    sys.exit(1)
 commands[cmd](sys.argv[2:])
 commands[cmd](sys.argv[2:])

+ 8 - 0
debian/changelog

@@ -1,3 +1,11 @@
+dulwich (0.4.1-1) unstable; urgency=low
+
+  * New upstream release.
+   + Supports time-less tags. Closes: #543240
+  * Rebuilt against newer version of python-central. Closes: #551893
+
+ -- Jelmer Vernooij <jelmer@debian.org>  Sun, 03 Jan 2010 16:40:19 +0100
+
 dulwich (0.4.0-1) unstable; urgency=low
 dulwich (0.4.0-1) unstable; urgency=low
 
 
   * New upstream release.
   * New upstream release.

+ 1 - 1
docs/tutorial/2-change-file.txt

@@ -55,7 +55,7 @@ previous blob recorded as "spam".
 You won't see it using git log because the head is still the previous
 You won't see it using git log because the head is still the previous
 commit. It's easy to remedy::
 commit. It's easy to remedy::
 
 
-  >>> repo.refs['refs/heads/master'] = commit.id
+  >>> repo.refs['refs/heads/master'] = c2.id
 
 
 Now all git tools will work as expected. Though don't forget that Dulwich is
 Now all git tools will work as expected. Though don't forget that Dulwich is
 still open!
 still open!

+ 1 - 1
dulwich/__init__.py

@@ -27,4 +27,4 @@ import protocol
 import repo
 import repo
 import server
 import server
 
 
-__version__ = (0, 4, 0)
+__version__ = (0, 4, 1)

+ 19 - 0
dulwich/client.py

@@ -124,6 +124,25 @@ class GitClient(object):
             
             
         return new_refs
         return new_refs
 
 
+    def fetch(self, path, target, determine_wants=None, progress=None):
+        """Fetch into a target repository.
+
+        :param path: Path to fetch from
+        :param target: Target repository to fetch into
+        :param determine_wants: Optional function to determine what refs 
+            to fetch
+        :param progress: Optional progress function
+        :return: remote refs
+        """
+        if determine_wants is None:
+            determine_wants = target.object_store.determine_wants_all
+        f, commit = target.object_store.add_pack()
+        try:
+            return self.fetch_pack(path, determine_wants, target.graph_walker, 
+                                   f.write, progress)
+        finally:
+            commit()
+
     def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
     def fetch_pack(self, path, determine_wants, graph_walker, pack_data,
                    progress):
                    progress):
         """Retrieve a pack from a git smart server.
         """Retrieve a pack from a git smart server.

+ 28 - 1
dulwich/index.py

@@ -212,9 +212,14 @@ class Index(object):
         """Return the (git object) SHA1 for the object at a path."""
         """Return the (git object) SHA1 for the object at a path."""
         return self[path][-2]
         return self[path][-2]
 
 
+    def get_mode(self, path):
+        """Return the POSIX file mode for the object at a path."""
+        return self[path][-6]
+
     def iterblobs(self):
     def iterblobs(self):
         """Iterate over path, sha, mode tuples for use with commit_tree."""
         """Iterate over path, sha, mode tuples for use with commit_tree."""
-        for path, entry in self:
+        for path in self:
+            entry = self[path]
             yield path, entry[-2], cleanup_mode(entry[-6])
             yield path, entry[-2], cleanup_mode(entry[-6])
 
 
     def clear(self):
     def clear(self):
@@ -234,6 +239,28 @@ class Index(object):
         for name, value in entries.iteritems():
         for name, value in entries.iteritems():
             self[name] = value
             self[name] = value
 
 
+    def changes_from_tree(self, object_store, tree, want_unchanged=False):
+        """Find the differences between the contents of this index and a tree.
+
+        :param object_store: Object store to use for retrieving tree contents
+        :param tree: SHA1 of the root tree
+        :param want_unchanged: Whether unchanged files should be reported
+        :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
+        """
+        mine = set(self._byname.keys())
+        for (name, mode, sha) in object_store.iter_tree_contents(tree):
+            if name in mine:
+                if (want_unchanged or self.get_sha1(name) != sha or 
+                    self.get_mode(name) != mode):
+                    yield ((name, name), (mode, self.get_mode(name)), (sha, self.get_sha1(name)))
+                mine.remove(name)
+            else:
+                # Was removed
+                yield ((name, None), (mode, None), (sha, None))
+        # Mention added files
+        for name in mine:
+            yield ((None, name), (None, self.get_mode(name)), (None, self.get_sha1(name)))
+
 
 
 def commit_tree(object_store, blobs):
 def commit_tree(object_store, blobs):
     """Commit a new tree.
     """Commit a new tree.

+ 85 - 7
dulwich/object_store.py

@@ -1,16 +1,16 @@
-# object_store.py -- Object store for git objects 
+# object_store.py -- Object store for git objects
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
-# 
+#
 # This program is free software; you can redistribute it and/or
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 # modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; either version 2
 # as published by the Free Software Foundation; either version 2
 # or (at your option) a later version of the License.
 # or (at your option) a later version of the License.
-# 
+#
 # This program is distributed in the hope that it will be useful,
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 # GNU General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
@@ -40,8 +40,8 @@ from dulwich.objects import (
     )
     )
 from dulwich.pack import (
 from dulwich.pack import (
     Pack,
     Pack,
-    PackData, 
-    iter_sha1, 
+    PackData,
+    iter_sha1,
     load_pack_index,
     load_pack_index,
     write_pack,
     write_pack,
     write_pack_data,
     write_pack_data,
@@ -71,7 +71,7 @@ class BaseObjectStore(object):
 
 
     def get_raw(self, name):
     def get_raw(self, name):
         """Obtain the raw text for an object.
         """Obtain the raw text for an object.
-        
+
         :param name: sha for the object.
         :param name: sha for the object.
         :return: tuple with object type and object contents.
         :return: tuple with object type and object contents.
         """
         """
@@ -99,6 +99,84 @@ class BaseObjectStore(object):
         """
         """
         raise NotImplementedError(self.add_objects)
         raise NotImplementedError(self.add_objects)
 
 
+    def tree_changes(self, source, target, want_unchanged=False):
+        """Find the differences between the contents of two trees
+
+        :param object_store: Object store to use for retrieving tree contents
+        :param tree: SHA1 of the root tree
+        :param want_unchanged: Whether unchanged files should be reported
+        :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
+        """
+        todo = set([(source, target, "")])
+        while todo:
+            (sid, tid, path) = todo.pop()
+            if sid is not None:
+                stree = self[sid]
+            else:
+                stree = {}
+            if tid is not None:
+                ttree = self[tid]
+            else:
+                ttree = {}
+            for name, oldmode, oldhexsha in stree.iteritems():
+                if path == "":
+                    oldchildpath = name
+                else:
+                    oldchildpath = "%s/%s" % (path, name)
+                try:
+                    (newmode, newhexsha) = ttree[name]
+                    newchildpath = oldchildpath
+                except KeyError:
+                    newmode = None
+                    newhexsha = None
+                    newchildpath = None
+                if (want_unchanged or oldmode != newmode or 
+                    oldhexsha != newhexsha):
+                    if stat.S_ISDIR(oldmode):
+                        if newmode is None or stat.S_ISDIR(newmode):
+                            todo.add((oldhexsha, newhexsha, oldchildpath))
+                        else:
+                            # entry became a file
+                            todo.add((oldhexsha, None, oldchildpath))
+                            yield ((None, newchildpath), (None, newmode), (None, newhexsha))
+                    else:
+                        if newmode is not None and stat.S_ISDIR(newmode):
+                            # entry became a dir
+                            yield ((oldchildpath, None), (oldmode, None), (oldhexsha, None))
+                            todo.add((None, newhexsha, newchildpath))
+                        else:
+                            yield ((oldchildpath, newchildpath), (oldmode, newmode), (oldhexsha, newhexsha))
+
+            for name, newmode, newhexsha in ttree.iteritems():
+                if path == "":
+                    childpath = name
+                else:
+                    childpath = "%s/%s" % (path, name)
+                if not name in stree:
+                    if not stat.S_ISDIR(newmode):
+                        yield ((None, childpath), (None, newmode), (None, newhexsha))
+                    else:
+                        todo.add((None, newhexsha, childpath))
+
+    def iter_tree_contents(self, tree):
+        """Yield (path, mode, hexsha) tuples for all non-Tree objects in a tree.
+
+        :param tree: SHA1 of the root of the tree
+        """
+        todo = set([(tree, "")])
+        while todo:
+            (tid, tpath) = todo.pop()
+            tree = self[tid]
+            for name, mode, hexsha in tree.iteritems(): 
+                if tpath == "":
+                    path = name
+                else:
+                    path = "%s/%s" % (tpath, name)
+                if stat.S_ISDIR(mode):
+                    todo.add((hexsha, path))
+                else:
+                    yield path, mode, hexsha
+
     def find_missing_objects(self, haves, wants, progress=None):
     def find_missing_objects(self, haves, wants, progress=None):
         """Find the missing objects required for a set of revisions.
         """Find the missing objects required for a set of revisions.
 
 

+ 58 - 56
dulwich/objects.py

@@ -1,17 +1,17 @@
 # objects.py -- Access to base git objects
 # objects.py -- Access to base git objects
 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
 # Copyright (C) 2007 James Westby <jw+debian@jameswestby.net>
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
-# 
+#
 # This program is free software; you can redistribute it and/or
 # This program is free software; you can redistribute it and/or
 # modify it under the terms of the GNU General Public License
 # modify it under the terms of the GNU General Public License
 # as published by the Free Software Foundation; version 2
 # as published by the Free Software Foundation; version 2
 # of the License or (at your option) a later version of the License.
 # of the License or (at your option) a later version of the License.
-# 
+#
 # This program is distributed in the hope that it will be useful,
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 # GNU General Public License for more details.
 # GNU General Public License for more details.
-# 
+#
 # You should have received a copy of the GNU General Public License
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
@@ -89,7 +89,7 @@ def serializable_property(name, docstring=None):
 
 
 class ShaFile(object):
 class ShaFile(object):
     """A git SHA file."""
     """A git SHA file."""
-  
+
     @classmethod
     @classmethod
     def _parse_legacy_object(cls, map):
     def _parse_legacy_object(cls, map):
         """Parse a legacy object, creating it and setting object._text"""
         """Parse a legacy object, creating it and setting object._text"""
@@ -120,7 +120,7 @@ class ShaFile(object):
     def as_legacy_object(self):
     def as_legacy_object(self):
         text = self.as_raw_string()
         text = self.as_raw_string()
         return zlib.compress("%s %d\0%s" % (self._type, len(text), text))
         return zlib.compress("%s %d\0%s" % (self._type, len(text), text))
-  
+
     def as_raw_string(self):
     def as_raw_string(self):
         if self._needs_serialization:
         if self._needs_serialization:
             self.serialize()
             self.serialize()
@@ -146,7 +146,7 @@ class ShaFile(object):
         self._sha = None
         self._sha = None
         self._needs_parsing = True
         self._needs_parsing = True
         self._needs_serialization = False
         self._needs_serialization = False
-  
+
     @classmethod
     @classmethod
     def _parse_object(cls, map):
     def _parse_object(cls, map):
         """Parse a new style object , creating it and setting object._text"""
         """Parse a new style object , creating it and setting object._text"""
@@ -164,7 +164,7 @@ class ShaFile(object):
         raw = map[used:]
         raw = map[used:]
         object.set_raw_string(_decompress(raw))
         object.set_raw_string(_decompress(raw))
         return object
         return object
-  
+
     @classmethod
     @classmethod
     def _parse_file(cls, map):
     def _parse_file(cls, map):
         word = (ord(map[0]) << 8) + ord(map[1])
         word = (ord(map[0]) << 8) + ord(map[1])
@@ -172,14 +172,14 @@ class ShaFile(object):
             return cls._parse_legacy_object(map)
             return cls._parse_legacy_object(map)
         else:
         else:
             return cls._parse_object(map)
             return cls._parse_object(map)
-  
+
     def __init__(self):
     def __init__(self):
         """Don't call this directly"""
         """Don't call this directly"""
         self._sha = None
         self._sha = None
-  
+
     def _parse_text(self):
     def _parse_text(self):
         """For subclasses to do initialisation time parsing"""
         """For subclasses to do initialisation time parsing"""
-  
+
     @classmethod
     @classmethod
     def from_file(cls, filename):
     def from_file(cls, filename):
         """Get the contents of a SHA file on disk"""
         """Get the contents of a SHA file on disk"""
@@ -191,11 +191,11 @@ class ShaFile(object):
             return shafile
             return shafile
         finally:
         finally:
             f.close()
             f.close()
-  
+
     @classmethod
     @classmethod
     def from_raw_string(cls, type, string):
     def from_raw_string(cls, type, string):
         """Creates an object of the indicated type from the raw string given.
         """Creates an object of the indicated type from the raw string given.
-    
+
         Type is the numeric type of an object. String is the raw uncompressed
         Type is the numeric type of an object. String is the raw uncompressed
         contents.
         contents.
         """
         """
@@ -204,10 +204,10 @@ class ShaFile(object):
         obj.type = type
         obj.type = type
         obj.set_raw_string(string)
         obj.set_raw_string(string)
         return obj
         return obj
-  
+
     def _header(self):
     def _header(self):
         return "%s %lu\0" % (self._type, len(self.as_raw_string()))
         return "%s %lu\0" % (self._type, len(self.as_raw_string()))
-  
+
     def sha(self):
     def sha(self):
         """The SHA1 object that is the name of this object."""
         """The SHA1 object that is the name of this object."""
         if self._needs_serialization or self._sha is None:
         if self._needs_serialization or self._sha is None:
@@ -215,11 +215,11 @@ class ShaFile(object):
             self._sha.update(self._header())
             self._sha.update(self._header())
             self._sha.update(self.as_raw_string())
             self._sha.update(self.as_raw_string())
         return self._sha
         return self._sha
-  
+
     @property
     @property
     def id(self):
     def id(self):
         return self.sha().hexdigest()
         return self.sha().hexdigest()
-  
+
     def get_type(self):
     def get_type(self):
         return self._num_type
         return self._num_type
 
 
@@ -227,16 +227,16 @@ class ShaFile(object):
         self._num_type = type
         self._num_type = type
 
 
     type = property(get_type, set_type)
     type = property(get_type, set_type)
-  
+
     def __repr__(self):
     def __repr__(self):
         return "<%s %s>" % (self.__class__.__name__, self.id)
         return "<%s %s>" % (self.__class__.__name__, self.id)
 
 
     def __ne__(self, other):
     def __ne__(self, other):
         return self.id != other.id
         return self.id != other.id
-  
+
     def __eq__(self, other):
     def __eq__(self, other):
         """Return true id the sha of the two objects match.
         """Return true id the sha of the two objects match.
-  
+
         The __le__ etc methods aren't overriden as they make no sense,
         The __le__ etc methods aren't overriden as they make no sense,
         certainly at this level.
         certainly at this level.
         """
         """
@@ -257,7 +257,7 @@ class Blob(ShaFile):
     def set_data(self, data):
     def set_data(self, data):
         self._text = data
         self._text = data
 
 
-    data = property(get_data, set_data, 
+    data = property(get_data, set_data,
             "The text contained within the blob object.")
             "The text contained within the blob object.")
 
 
     @classmethod
     @classmethod
@@ -306,7 +306,10 @@ class Tag(ShaFile):
         f.write("%s %s\n" % (TYPE_ID, num_type_map[self._object_type]._type))
         f.write("%s %s\n" % (TYPE_ID, num_type_map[self._object_type]._type))
         f.write("%s %s\n" % (TAG_ID, self._name))
         f.write("%s %s\n" % (TAG_ID, self._name))
         if self._tagger:
         if self._tagger:
-            f.write("%s %s %d %s\n" % (TAGGER_ID, self._tagger, self._tag_time, format_timezone(self._tag_timezone)))
+            if self._tag_time is None:
+                f.write("%s %s\n" % (TAGGER_ID, self._tagger))
+            else:
+                f.write("%s %s %d %s\n" % (TAGGER_ID, self._tagger, self._tag_time, format_timezone(self._tag_timezone)))
         f.write("\n") # To close headers
         f.write("\n") # To close headers
         f.write(self._message)
         f.write(self._message)
         self._text = f.getvalue()
         self._text = f.getvalue()
@@ -328,14 +331,20 @@ class Tag(ShaFile):
             elif field == TAG_ID:
             elif field == TAG_ID:
                 self._name = value
                 self._name = value
             elif field == TAGGER_ID:
             elif field == TAGGER_ID:
-                sep = value.index("> ")
-                self._tagger = value[0:sep+1]
-                (timetext, timezonetext) = value[sep+2:].rsplit(" ", 1)
                 try:
                 try:
-                    self._tag_time = int(timetext)
-                except ValueError: #Not a unix timestamp
-                    self._tag_time = time.strptime(timetext)
-                self._tag_timezone = parse_timezone(timezonetext)
+                    sep = value.index("> ")
+                except ValueError:
+                    self._tagger = value
+                    self._tag_time = None
+                    self._tag_timezone = None
+                else:
+                    self._tagger = value[0:sep+1]
+                    (timetext, timezonetext) = value[sep+2:].rsplit(" ", 1)
+                    try:
+                        self._tag_time = int(timetext)
+                    except ValueError: #Not a unix timestamp
+                        self._tag_time = time.strptime(timetext)
+                    self._tag_timezone = parse_timezone(timezonetext)
             else:
             else:
                 raise AssertionError("Unknown field %s" % field)
                 raise AssertionError("Unknown field %s" % field)
         self._message = f.read()
         self._message = f.read()
@@ -354,11 +363,11 @@ class Tag(ShaFile):
     object = property(get_object, set_object)
     object = property(get_object, set_object)
 
 
     name = serializable_property("name", "The name of this tag")
     name = serializable_property("name", "The name of this tag")
-    tagger = serializable_property("tagger", 
+    tagger = serializable_property("tagger",
         "Returns the name of the person who created this tag")
         "Returns the name of the person who created this tag")
-    tag_time = serializable_property("tag_time", 
+    tag_time = serializable_property("tag_time",
         "The creation timestamp of the tag.  As the number of seconds since the epoch")
         "The creation timestamp of the tag.  As the number of seconds since the epoch")
-    tag_timezone = serializable_property("tag_timezone", 
+    tag_timezone = serializable_property("tag_timezone",
         "The timezone that tag_time is in.")
         "The timezone that tag_time is in.")
     message = serializable_property("message", "The message attached to this tag")
     message = serializable_property("message", "The message attached to this tag")
 
 
@@ -366,27 +375,20 @@ class Tag(ShaFile):
 def parse_tree(text):
 def parse_tree(text):
     ret = {}
     ret = {}
     count = 0
     count = 0
-    while count < len(text):
-        mode = 0
-        chr = text[count]
-        while chr != ' ':
-            assert chr >= '0' and chr <= '7', "%s is not a valid mode char" % chr
-            mode = (mode << 3) + (ord(chr) - ord('0'))
-            count += 1
-            chr = text[count]
-        count += 1
-        chr = text[count]
-        name = ''
-        while chr != '\0':
-            name += chr
-            count += 1
-            chr = text[count]
-        count += 1
-        chr = text[count]
-        sha = text[count:count+20]
-        hexsha = sha_to_hex(sha)
-        ret[name] = (mode, hexsha)
-        count = count + 20
+    l = len(text)
+    while count < l:
+        mode_end = text.index(' ', count)
+        mode = int(text[count:mode_end], 8)
+
+        name_end = text.index('\0', mode_end)
+        name = text[mode_end+1:name_end]
+
+        count = name_end+21
+
+        sha = text[name_end+1:count]
+
+        ret[name] = (mode, sha_to_hex(sha))
+
     return ret
     return ret
 
 
 
 
@@ -576,10 +578,10 @@ class Commit(ShaFile):
 
 
     parents = property(get_parents, set_parents)
     parents = property(get_parents, set_parents)
 
 
-    author = serializable_property("author", 
+    author = serializable_property("author",
         "The name of the author of the commit")
         "The name of the author of the commit")
 
 
-    committer = serializable_property("committer", 
+    committer = serializable_property("committer",
         "The name of the committer of the commit")
         "The name of the committer of the commit")
 
 
     message = serializable_property("message",
     message = serializable_property("message",
@@ -591,10 +593,10 @@ class Commit(ShaFile):
     commit_timezone = serializable_property("commit_timezone",
     commit_timezone = serializable_property("commit_timezone",
         "The zone the commit time is in")
         "The zone the commit time is in")
 
 
-    author_time = serializable_property("author_time", 
+    author_time = serializable_property("author_time",
         "The timestamp the commit was written. as the number of seconds since the epoch.")
         "The timestamp the commit was written. as the number of seconds since the epoch.")
 
 
-    author_timezone = serializable_property("author_timezone", 
+    author_timezone = serializable_property("author_timezone",
         "Returns the zone the author time is in.")
         "Returns the zone the author time is in.")
 
 
     encoding = serializable_property("encoding",
     encoding = serializable_property("encoding",

+ 110 - 0
dulwich/patch.py

@@ -0,0 +1,110 @@
+# patch.py -- For dealing wih packed-style patches.
+# Copryight (C) 2009 Jelmer Vernooij <jelmer@samba.org>
+# 
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; version 2
+# of the License or (at your option) a later version.
+# 
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+# 
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+# MA  02110-1301, USA.
+
+"""Classes for dealing with git am-style patches.
+
+These patches are basically unified diffs with some extra metadata tacked 
+on.
+"""
+
+import difflib
+import subprocess
+import time
+
+
+def write_commit_patch(f, commit, contents, progress, version=None):
+    """Write a individual file patch.
+
+    :param commit: Commit object
+    :param progress: Tuple with current patch number and total.
+    :return: tuple with filename and contents
+    """
+    (num, total) = progress
+    f.write("From %s %s\n" % (commit.id, time.ctime(commit.commit_time)))
+    f.write("From: %s\n" % commit.author)
+    f.write("Date: %s\n" % time.strftime("%a, %d %b %Y %H:%M:%S %Z"))
+    f.write("Subject: [PATCH %d/%d] %s\n" % (num, total, commit.message))
+    f.write("\n")
+    f.write("---\n")
+    try:
+        p = subprocess.Popen(["diffstat"], stdout=subprocess.PIPE, 
+                             stdin=subprocess.PIPE)
+    except OSError, e:
+        pass # diffstat not available?
+    else:
+        (diffstat, _) = p.communicate(contents)
+        f.write(diffstat)
+        f.write("\n")
+    f.write(contents)
+    f.write("-- \n")
+    if version is None:
+        from dulwich import __version__ as dulwich_version
+        f.write("Dulwich %d.%d.%d\n" % dulwich_version)
+    else:
+        f.write("%s\n" % version)
+
+
+def get_summary(commit):
+    """Determine the summary line for use in a filename.
+    
+    :param commit: Commit
+    :return: Summary string
+    """
+    return commit.message.splitlines()[0].replace(" ", "-")
+
+
+def write_blob_diff(f, (old_path, old_mode, old_blob), 
+                       (new_path, new_mode, new_blob)):
+    """Write diff file header.
+
+    :param f: File-like object to write to
+    :param (old_path, old_mode, old_blob): Previous file (None if nonexisting)
+    :param (new_path, new_mode, new_blob): New file (None if nonexisting)
+    """
+    def blob_id(blob):
+        if blob is None:
+            return "0" * 7
+        else:
+            return blob.id[:7]
+    def lines(blob):
+        if blob is not None:
+            return blob.data.splitlines(True)
+        else:
+            return []
+    if old_path is None:
+        old_path = "/dev/null"
+    else:
+        old_path = "a/%s" % old_path
+    if new_path is None:
+        new_path = "/dev/null"
+    else:
+        new_path = "b/%s" % new_path
+    f.write("diff --git %s %s\n" % (old_path, new_path))
+    if old_mode != new_mode:
+        if new_mode is not None:
+            if old_mode is not None:
+                f.write("old file mode %o\n" % old_mode)
+            f.write("new file mode %o\n" % new_mode) 
+        else:
+            f.write("deleted file mode %o\n" % old_mode)
+    f.write("index %s..%s %o\n" % (
+        blob_id(old_blob), blob_id(new_blob), new_mode))
+    old_contents = lines(old_blob)
+    new_contents = lines(new_blob)
+    f.writelines(difflib.unified_diff(old_contents, new_contents, 
+        old_path, new_path))

+ 59 - 0
dulwich/repo.py

@@ -242,6 +242,19 @@ class Repo(object):
         """Check if an index is present."""
         """Check if an index is present."""
         return os.path.exists(self.index_path())
         return os.path.exists(self.index_path())
 
 
+    def fetch(self, target, determine_wants=None, progress=None):
+        """Fetch objects into another repository.
+
+        :param target: The target repository
+        :param determine_wants: Optional function to determine what refs to 
+            fetch.
+        :param progress: Optional progress function
+        """
+        target.object_store.add_objects(
+            self.fetch_objects(determine_wants, target.get_graph_walker(),
+                progress))
+        return self.get_refs()
+
     def fetch_objects(self, determine_wants, graph_walker, progress):
     def fetch_objects(self, determine_wants, graph_walker, progress):
         """Fetch the missing objects required for a set of revisions.
         """Fetch the missing objects required for a set of revisions.
 
 
@@ -396,6 +409,52 @@ class Repo(object):
             del self.refs[name]
             del self.refs[name]
         raise ValueError(name)
         raise ValueError(name)
 
 
+    def do_commit(self, committer, message,
+                  author=None, commit_timestamp=None,
+                  commit_timezone=None, author_timestamp=None, 
+                  author_timezone=None, tree=None):
+        """Create a new commit.
+
+        :param committer: Committer fullname
+        :param message: Commit message
+        :param author: Author fullname (defaults to committer)
+        :param commit_timestamp: Commit timestamp (defaults to now)
+        :param commit_timezone: Commit timestamp timezone (defaults to GMT)
+        :param author_timestamp: Author timestamp (defaults to commit timestamp)
+        :param author_timezone: Author timestamp timezone 
+            (defaults to commit timestamp timezone)
+        :param tree: SHA1 of the tree root to use (if not specified the current index will be committed).
+        :return: New commit SHA1
+        """
+        from dulwich.index import commit_index
+        import time
+        index = self.open_index()
+        c = Commit()
+        if tree is None:
+            c.tree = commit_index(self.object_store, index)
+        else:
+            c.tree = tree
+        c.committer = committer
+        if commit_timestamp is None:
+            commit_timestamp = time.time()
+        c.commit_time = int(commit_timestamp)
+        if commit_timezone is None:
+            commit_timezone = 0
+        c.commit_timezone = commit_timezone
+        if author is None:
+            author = committer
+        c.author = author
+        if author_timestamp is None:
+            author_timestamp = commit_timestamp
+        c.author_time = int(author_timestamp)
+        if author_timezone is None:
+            author_timezone = commit_timezone
+        c.author_timezone = author_timezone
+        c.message = message
+        self.object_store.add_object(c)
+        self.refs["HEAD"] = c.id
+        return c.id
+
     @classmethod
     @classmethod
     def init(cls, path, mkdir=True):
     def init(cls, path, mkdir=True):
         controldir = os.path.join(path, ".git")
         controldir = os.path.join(path, ".git")

+ 7 - 4
setup.py

@@ -2,10 +2,13 @@
 # Setup file for bzr-git
 # Setup file for bzr-git
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
 # Copyright (C) 2008-2009 Jelmer Vernooij <jelmer@samba.org>
 
 
-from distutils.core import setup
+try:
+    from setuptools import setup
+except ImportError:
+    from distutils.core import setup
 from distutils.extension import Extension
 from distutils.extension import Extension
 
 
-dulwich_version_string = '0.4.0'
+dulwich_version_string = '0.4.1'
 
 
 include_dirs = []
 include_dirs = []
 # Windows MSVC support
 # Windows MSVC support
@@ -24,8 +27,8 @@ setup(name='dulwich',
       author='Jelmer Vernooij',
       author='Jelmer Vernooij',
       author_email='jelmer@samba.org',
       author_email='jelmer@samba.org',
       long_description="""
       long_description="""
-      Simple Pure-Python implementation of the Git file formats and 
-      protocols. Dulwich is the place where Mr. and Mrs. Git live 
+      Simple Pure-Python implementation of the Git file formats and
+      protocols. Dulwich is the place where Mr. and Mrs. Git live
       in one of the Monty Python sketches.
       in one of the Monty Python sketches.
       """,
       """,
       packages=['dulwich', 'dulwich.tests'],
       packages=['dulwich', 'dulwich.tests'],