Browse Source

Merge lp:~johncarr/dulwich/git-serve

John Carr 16 years ago
parent
commit
ada58962e6

+ 22 - 10
bin/dulwich

@@ -20,16 +20,26 @@
 import sys
 from getopt import getopt
 
+def get_transport_and_path(uri):
+    from dulwich.client import TCPGitClient, SSHGitClient
+    for handler, transport in (("git://", TCPGitClient), ("git+ssh://", SSHGitClient)):
+        if uri.startswith(handler):
+            host, path = uri[len(handler):].split("/", 1)
+            return transport(host), "/"+path
+    return None, None
+
 def cmd_fetch_pack(args):
-	from dulwich.client import TCPGitClient, SimpleFetchGraphWalker
+	from dulwich.client import SimpleFetchGraphWalker
 	from dulwich.repo import Repo
 	opts, args = getopt(args, "", ["all"])
 	opts = dict(opts)
 	if not ":" in args[0]:
 		print "Usage: dulwich fetch-pack [--all] host:path [REF...]"
 		sys.exit(1)
-	(host, path) = args.pop(0).split(":", 1)
-	client = TCPGitClient(host)
+        client, path = get_transport_and_path(args.pop(0))
+        if not client:
+            print "Must be git:// or git+ssh://"
+            sys.exit(1)
 	if "--all" in opts:
 		determine_wants = lambda x: [y for y in x.values() if not y in r.object_store]
 	else:
@@ -137,7 +147,7 @@ def cmd_init(args):
 
 
 def cmd_clone(args):
-	from dulwich.client import TCPGitClient, SubprocessGitClient, SimpleFetchGraphWalker
+	from dulwich.client import SimpleFetchGraphWalker
 	from dulwich.repo import Repo
 	import os
 	import sys
@@ -148,12 +158,14 @@ def cmd_clone(args):
 		print "usage: dulwich clone host:path [PATH]"
 		sys.exit(1)
 
-	#if not ":" in args[0]:
-	#	print "Usage: dulwich clone host:path [PATH]"
-	#	sys.exit(1)
-	(host, host_path) = args.pop(0).split(":", 1)
-	#client = TCPGitClient(host)
-	client = SubprocessGitClient(host)
+	if not ":" in args[0]:
+		print "Usage: dulwich clone host:path [PATH]"
+		sys.exit(1)
+        client, host_path = get_transport_and_path(args.pop(0))
+        if not client:
+            print "Must be git:// or git+ssh://"
+            sys.exit(1)
+
 	if len(args) > 0:
 		path = args.pop(0)
 	else:

+ 4 - 1
dulwich/__init__.py

@@ -4,7 +4,8 @@
 # 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.
+# of the License or (at your option) any later version of 
+# the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -20,3 +21,5 @@ import client
 import protocol
 import repo
 import server
+
+__version__ = (0, 1, 0)

+ 56 - 2
dulwich/client.py

@@ -3,8 +3,8 @@
 #
 # 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.
+# as published by the Free Software Foundation; either version 2
+# or (at your option) a later version of the License.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -180,3 +180,57 @@ class SubprocessGitClient(GitClient):
         client = self._connect("git-upload-pack", path)
         client.fetch_pack(path, determine_wants, graph_walker, pack_data, progress)
 
+
+class SSHSubprocess(object):
+    """A socket-like object that talks to an ssh subprocess via pipes."""
+
+    def __init__(self, proc):
+        self.proc = proc
+
+    def send(self, data):
+        return os.write(self.proc.stdin.fileno(), data)
+
+    def recv(self, count):
+        return os.read(self.proc.stdout.fileno(), count)
+
+    def close(self):
+        self.proc.stdin.close()
+        self.proc.stdout.close()
+        self.proc.wait()
+
+
+class SSHVendor(object):
+
+    def connect_ssh(self, host, command, username=None, port=None):
+        #FIXME: This has no way to deal with passwords..
+        args = ['ssh', '-x']
+        if port is not None:
+            args.extend(['-p', str(port)])
+        if username is not None:
+            host = "%s@%s" % (username, host)
+        args.append(host)
+        proc = subprocess.Popen(args + command,
+                                stdin=subprocess.PIPE,
+                                stdout=subprocess.PIPE)
+        return SSHSubprocess(proc)
+
+# Can be overridden by users
+get_ssh_vendor = SSHVendor
+
+
+class SSHGitClient(GitClient):
+
+    def __init__(self, host, port=None):
+        self.host = host
+        self.port = port
+
+    def send_pack(self, path):
+        remote = get_ssh_vendor().connect_ssh(self.host, ["git-receive-pack %s" % path], port=self.port)
+        client = GitClient(remote.proc.stdin.fileno(), remote.recv, remote.send)
+        client.send_pack(path)
+
+    def fetch_pack(self, path, determine_wants, graph_walker, pack_data, progress):
+        remote = get_ssh_vendor().connect_ssh(self.host, ["git-upload-pack %s" % path], port=self.port)
+        client = GitClient(remote.proc.stdin.fileno(), remote.recv, remote.send)
+        client.fetch_pack(path, determine_wants, graph_walker, pack_data, progress)
+

+ 2 - 2
dulwich/commit.py

@@ -3,8 +3,8 @@
 # 
 # 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.
+# as published by the Free Software Foundation; either version 2
+# or (at your option) any later version of the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 1 - 1
dulwich/errors.py

@@ -4,7 +4,7 @@
 # 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) any later version of the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 35 - 3
dulwich/object_store.py

@@ -3,8 +3,8 @@
 # 
 # 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.
+# as published by the Free Software Foundation; either version 2
+# or (at your option) a later version of the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -18,6 +18,7 @@
 
 from objects import (
         ShaFile,
+        hex_to_sha,
         )
 import os, tempfile
 from pack import (
@@ -27,6 +28,7 @@ from pack import (
         PackData, 
         )
 import tempfile
+import urllib2
 PACKDIR = 'pack'
 
 class ObjectStore(object):
@@ -86,6 +88,23 @@ class ObjectStore(object):
         type, uncomp = self.get_raw(sha)
         return ShaFile.from_raw_string(type, uncomp)
 
+    def move_in_thin_pack(self, path):
+        """Move a specific file containing a pack into the pack directory.
+
+        :note: The file should be on the same file system as the 
+            packs directory.
+
+        :param path: Path to the pack file.
+        """
+        p = PackData(path)
+        temppath = os.path.join(self.pack_dir(), sha_to_hex(urllib2.randombytes(20))+".temppack")
+        write_pack(temppath, p.iterobjects(self.get_raw), len(p))
+        pack_sha = PackIndex(temppath+".idx").objects_sha1()
+        os.rename(temppath+".pack", 
+            os.path.join(self.pack_dir(), "pack-%s.pack" % pack_sha))
+        os.rename(temppath+".idx", 
+            os.path.join(self.pack_dir(), "pack-%s.idx" % pack_sha))
+
     def move_in_pack(self, path):
         """Move a specific file containing a pack into the pack directory.
 
@@ -95,12 +114,25 @@ class ObjectStore(object):
         :param path: Path to the pack file.
         """
         p = PackData(path)
-        entries = p.sorted_entries(self.get_raw)
+        entries = p.sorted_entries()
         basename = os.path.join(self.pack_dir(), 
             "pack-%s" % iter_sha1(entry[0] for entry in entries))
         write_pack_index_v2(basename+".idx", entries, p.calculate_checksum())
         os.rename(path, basename + ".pack")
 
+    def add_thin_pack(self):
+        """Add a new thin pack to this object store.
+
+        Thin packs are packs that contain deltas with parents that exist 
+        in a different pack.
+        """
+        fd, path = tempfile.mkstemp(dir=self.pack_dir(), suffix=".pack")
+        f = os.fdopen(fd, 'w')
+        def commit():
+            if os.path.getsize(path) > 0:
+                self.move_in_thin_pack(path)
+        return f, commit
+
     def add_pack(self):
         """Add a new pack to this object store. 
 

+ 100 - 0
dulwich/objects.py

@@ -37,6 +37,9 @@ COMMIT_ID = "commit"
 PARENT_ID = "parent"
 AUTHOR_ID = "author"
 COMMITTER_ID = "committer"
+OBJECT_ID = "object"
+TYPE_ID = "type"
+TAGGER_ID = "tagger"
 
 def _decompress(string):
     dcomp = zlib.decompressobj()
@@ -170,6 +173,10 @@ class ShaFile(object):
   def id(self):
       return self.sha().hexdigest()
 
+  @property
+  def type(self):
+      return self._num_type
+
   def __repr__(self):
     return "<%s %s>" % (self.__class__.__name__, self.id)
 
@@ -227,6 +234,99 @@ class Tag(ShaFile):
     shafile._text = string
     return shafile
 
+  def _parse_text(self):
+    """Grab the metadata attached to the tag"""
+    text = self._text
+    count = 0
+    assert text.startswith(OBJECT_ID), "Invalid tag object, " \
+         "must start with %s" % OBJECT_ID
+    count += len(OBJECT_ID)
+    assert text[count] == ' ', "Invalid tag object, " \
+         "%s must be followed by space not %s" % (OBJECT_ID, text[count])
+    count += 1
+    self._object_sha = text[count:count+40]
+    count += 40
+    assert text[count] == '\n', "Invalid tag object, " \
+         "%s sha must be followed by newline" % OBJECT_ID
+    count += 1
+    assert text[count:].startswith(TYPE_ID), "Invalid tag object, " \
+         "%s sha must be followed by %s" % (OBJECT_ID, TYPE_ID)
+    count += len(TYPE_ID)
+    assert text[count] == ' ', "Invalid tag object, " \
+        "%s must be followed by space not %s" % (TAG_ID, text[count])
+    count += 1
+    self._object_type = ""
+    while text[count] != '\n':
+        self._object_type += text[count]
+        count += 1
+    count += 1
+    assert self._object_type in (COMMIT_ID, BLOB_ID, TREE_ID, TAG_ID), "Invalid tag object, " \
+        "unexpected object type %s" % self._object_type
+    self._object_type = type_map[self._object_type]
+
+    assert text[count:].startswith(TAG_ID), "Invalid tag object, " \
+        "object type must be followed by %s" % (TAG_ID)
+    count += len(TAG_ID)
+    assert text[count] == ' ', "Invalid tag object, " \
+        "%s must be followed by space not %s" % (TAG_ID, text[count])
+    count += 1
+    self._name = ""
+    while text[count] != '\n':
+        self._name += text[count]
+        count += 1
+    count += 1
+
+    assert text[count:].startswith(TAGGER_ID), "Invalid tag object, " \
+        "%s must be followed by %s" % (TAG_ID, TAGGER_ID)
+    count += len(TAGGER_ID)
+    assert text[count] == ' ', "Invalid tag object, " \
+        "%s must be followed by space not %s" % (TAGGER_ID, text[count])
+    count += 1
+    self._tagger = ""
+    while text[count] != '>':
+        assert text[count] != '\n', "Malformed tagger information"
+        self._tagger += text[count]
+        count += 1
+    self._tagger += text[count]
+    count += 1
+    assert text[count] == ' ', "Invalid tag object, " \
+        "tagger information must be followed by space not %s" % text[count]
+    count += 1
+    self._tag_time = int(text[count:count+10])
+    while text[count] != '\n':
+        count += 1
+    count += 1
+    assert text[count] == '\n', "There must be a new line after the headers"
+    count += 1
+    self._message = text[count:]
+
+  @property
+  def object(self):
+    """Returns the object pointed by this tag, represented as a tuple(type, sha)"""
+    return (self._object_type, self._object_sha)
+
+  @property
+  def name(self):
+    """Returns the name of this tag"""
+    return self._name
+
+  @property
+  def tagger(self):
+    """Returns the name of the person who created this tag"""
+    return self._tagger
+
+  @property
+  def tag_time(self):
+    """Returns the creation timestamp of the tag.
+
+    Returns it as the number of seconds since the epoch"""
+    return self._tag_time
+
+  @property
+  def message(self):
+    """Returns the message attached to this tag"""
+    return self._message
+
 
 class Tree(ShaFile):
   """A Git tree object"""

+ 24 - 26
dulwich/pack.py

@@ -589,8 +589,8 @@ def write_pack_data(f, objects, num_objects, window=10):
     # Build a list of objects ordered by the magic Linus heuristic
     # This helps us find good objects to diff against us
     magic = []
-    for o in recency:
-        magic.append( (o._num_type, "filename", 1, -len(o.as_raw_string()[1]), o) )
+    for obj, path in recency:
+        magic.append( (obj.type, path, 1, -len(obj.as_raw_string()[1]), obj) )
     magic.sort()
     # Build a map of objects and their index in magic - so we can find preceeding objects
     # to diff against
@@ -603,17 +603,22 @@ def write_pack_data(f, objects, num_objects, window=10):
     f.write("PACK")               # Pack header
     f.write(struct.pack(">L", 2)) # Pack version
     f.write(struct.pack(">L", num_objects)) # Number of objects in pack
-    for o in recency:
+    for o, path in recency:
         sha1 = o.sha().digest()
         crc32 = o.crc32()
-        t, raw = o.as_raw_string()
+        orig_t, raw = o.as_raw_string()
         winner = raw
-        for i in range(1, window+1):
-            base = magic[offs[sha1] - i]
-            delta = create_delta(base, raw)
-            if len(delta) < len(winner):
-                winner = delta
-        offset = write_pack_object(f, t, raw)
+        t = orig_t
+        #for i in range(offs[o]-window, window):
+        #    if i < 0 or i >= len(offs): continue
+        #    b = magic[i][4]
+        #    if b.type != orig_t: continue
+        #    _, base = b.as_raw_string()
+        #    delta = create_delta(base, raw)
+        #    if len(delta) < len(winner):
+        #        winner = delta
+        #        t = 6 if magic[i][2] == 1 else 7
+        offset = write_pack_object(f, t, winner)
         entries.append((sha1, offset, crc32))
     return entries, f.write_sha()
 
@@ -647,7 +652,6 @@ def create_delta(base_buf, target_buf):
     assert isinstance(base_buf, str)
     assert isinstance(target_buf, str)
     out_buf = ""
-
     # write delta header
     def encode_size(size):
         ret = ""
@@ -661,36 +665,29 @@ def create_delta(base_buf, target_buf):
         return ret
     out_buf += encode_size(len(base_buf))
     out_buf += encode_size(len(target_buf))
-
     # write out delta opcodes
     seq = difflib.SequenceMatcher(a=base_buf, b=target_buf)
     for opcode, i1, i2, j1, j2 in seq.get_opcodes():
         # Git patch opcodes don't care about deletes!
         #if opcode == "replace" or opcode == "delete":
         #    pass
-
         if opcode == "equal":
             # If they are equal, unpacker will use data from base_buf
             # Write out an opcode that says what range to use
             scratch = ""
             op = 0x80
-
             o = i1
             for i in range(4):
-                if o & 0x000000ff << i*8:
+                if o & 0xff << i*8:
                     scratch += chr(o >> i)
                     op |= 1 << i
-
             s = i2 - i1
             for i in range(2):
-                if s & 0x000000ff << i*8:
+                if s & 0xff << i*8:
                     scratch += chr(s >> i)
                     op |= 1 << (4+i)
-
             out_buf += chr(op)
             out_buf += scratch
-
-
         if opcode == "replace" or opcode == "insert":
             # If we are replacing a range or adding one, then we just
             # output it to the stream (prefixed by its size)
@@ -703,7 +700,6 @@ def create_delta(base_buf, target_buf):
                 o += 127
             out_buf += chr(s)
             out_buf += target_buf[o:o+s]
-
     return out_buf
 
 
@@ -852,8 +848,6 @@ class Pack(object):
         return (self.idx.object_index(sha1) is not None)
 
     def get_raw(self, sha1, resolve_ref=None):
-        if resolve_ref is None:
-            resolve_ref = self.get_raw
         offset = self.idx.object_index(sha1)
         if offset is None:
             raise KeyError(sha1)
@@ -868,12 +862,16 @@ class Pack(object):
         type, uncomp = self.get_raw(sha1)
         return ShaFile.from_raw_string(type, uncomp)
 
-    def iterobjects(self):
+    def iterobjects(self, get_raw=None):
+        if get_raw is None:
+            def get_raw(x):
+                raise KeyError(x)
         for offset, type, obj in self.data.iterobjects():
             assert isinstance(offset, int)
             yield ShaFile.from_raw_string(
-                    *resolve_object(offset, type, obj, self.get_raw, 
-                self.data.get_object_at))
+                    *resolve_object(offset, type, obj, 
+                        get_raw, 
+                    self.data.get_object_at))
 
 
 def load_packs(path):

+ 1 - 1
dulwich/protocol.py

@@ -5,7 +5,7 @@
 # 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) any later version of the License.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 43 - 25
dulwich/repo.py

@@ -5,7 +5,8 @@
 # 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.
+# of the License or (at your option) any later version of 
+# the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -17,7 +18,7 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 # MA  02110-1301, USA.
 
-import os
+import os, stat
 
 from commit import Commit
 from errors import (
@@ -39,11 +40,29 @@ OBJECTDIR = 'objects'
 SYMREF = 'ref: '
 
 
-class Tag(object):
+class Tags(object):
 
-    def __init__(self, name, ref):
-        self.name = name
-        self.ref = ref
+    def __init__(self, tagdir, tags):
+        self.tagdir = tagdir
+        self.tags = tags
+
+    def __getitem__(self, name):
+        return self.tags[name]
+    
+    def __setitem__(self, name, ref):
+        self.tags[name] = ref
+        f = open(os.path.join(self.tagdir, name), 'wb')
+        try:
+            f.write("%s\n" % ref)
+        finally:
+            f.close()
+
+    def __len__(self):
+        return len(self.tags)
+
+    def iteritems(self):
+        for k in self.tags:
+            yield k, self[k]
 
 
 class Repo(object):
@@ -60,7 +79,7 @@ class Repo(object):
     else:
       raise NotGitRepository(root)
     self.path = root
-    self.tags = [Tag(name, ref) for name, ref in self.get_tags().items()]
+    self.tags = Tags(self.tagdir(), self.get_tags())
     self._object_store = None
 
   def controldir(self):
@@ -82,36 +101,32 @@ class Repo(object):
     sha_done = set()
     ref = graph_walker.next()
     while ref:
-        sha_done.add(ref)
         if ref in self.object_store:
             graph_walker.ack(ref)
         ref = graph_walker.next()
     while commits_to_send:
-        sha = commits_to_send.pop()
+        sha = (commits_to_send.pop(), None)
         if sha in sha_done:
             continue
 
         c = self.commit(sha)
         assert isinstance(c, Commit)
-        sha_done.add(sha)
+        sha_done.add((sha, None))
 
         commits_to_send.update([p for p in c.parents if not p in sha_done])
 
         def parse_tree(tree, sha_done):
-            for mode, name, x in tree.entries():
-                if not x in sha_done:
-                    try:
-                        t = self.tree(x)
-                        sha_done.add(x)
-                        parse_tree(t, sha_done)
-                    except:
-                        sha_done.add(x)
+            for mode, name, sha in tree.entries():
+                if (sha, name) in sha_done:
+                    continue
+                if mode & stat.S_IFDIR:
+                    parse_tree(self.tree(sha), sha_done)
+                sha_done.add((sha, name))
 
         treesha = c.tree
-        if treesha not in sha_done:
-            t = self.tree(treesha)
-            sha_done.add(treesha)
-            parse_tree(t, sha_done)
+        if c.tree not in sha_done:
+            parse_tree(self.tree(c.tree), sha_done)
+            sha_done.add((c.tree, None))
 
         progress("counting objects: %d\r" % len(sha_done))
     return sha_done
@@ -126,10 +141,10 @@ class Repo(object):
         that a revision is present.
     :param progress: Simple progress function that will be called with 
         updated progress strings.
+    :return: tuple with number of objects, iterator over objects
     """
     shas = self.find_missing_objects(determine_wants, graph_walker, progress)
-    for sha in shas:
-        yield self.get_object(sha)
+    return (len(shas), ((self.get_object(sha), path) for sha, path in shas))
 
   def object_dir(self):
     return os.path.join(self.controldir(), OBJECTDIR)
@@ -184,9 +199,12 @@ class Repo(object):
       os.remove(file)
       return
 
+  def tagdir(self):
+    return os.path.join(self.controldir(), 'refs', 'tags')
+
   def get_tags(self):
     ret = {}
-    for root, dirs, files in os.walk(os.path.join(self.controldir(), 'refs', 'tags')):
+    for root, dirs, files in os.walk(self.tagdir()):
       for name in files:
         ret[name] = self._get_ref(os.path.join(root, name))
     return ret

+ 10 - 5
dulwich/server.py

@@ -4,7 +4,7 @@
 # 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) any later version of the License.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -63,7 +63,7 @@ class GitBackend(Backend):
         self.get_refs = self.repo.get_refs
 
     def apply_pack(self, refs, read):
-        fd, commit = self.repo.object_store.add_pack()
+        fd, commit = self.repo.object_store.add_thin_pack()
         fd.write(read())
         fd.close()
         commit()
@@ -145,10 +145,15 @@ class UploadPackHandler(Handler):
                 self.proto.write_pkt_line("NAK\n")
 
         graph_walker = ProtocolGraphWalker(self.proto)
-        objects = list(self.backend.fetch_objects(determine_wants, graph_walker, progress))
+        num_objects, objects_iter = self.backend.fetch_objects(determine_wants, graph_walker, progress)
+
+        # Do they want any objects?
+        if num_objects == 0:
+            return
+
         progress("dul-daemon says what\n")
-        progress("counting objects: %d, done.\n" % len(objects))
-        write_pack_data(ProtocolFile(None, write), objects, len(objects))
+        progress("counting objects: %d, done.\n" % num_objects)
+        write_pack_data(ProtocolFile(None, write), objects_iter, num_objects)
         progress("how was that, then?\n")
         # we are done
         self.proto.write("0000")

+ 2 - 1
dulwich/tests/__init__.py

@@ -4,7 +4,8 @@
 # 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.
+# of the License or (at your option) any later version of 
+# the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 4 - 0
dulwich/tests/data/tags/71033db03a03c6a36721efcf1968dd8f8e0cf023

@@ -0,0 +1,4 @@
+xmŽMOƒ@„=ï¯x�#Ý…²€Qã¶nIME”¢ÁÛòÙíòQ
+mÊ¿—ªG's˜L2O¦SzÑÄÛ,éÁ$1¥vžšqnaJb+¥¹0u3·mMr‘
+aÔ»’¦ªd�ú‘ÑÉ¢ÎÒs,²=°RB bYÂ�(¥Ö�ãcQ	YjãæˆnØ!p�	Æ­7²ƒÑâ�#	Ýœ5ãîÒßõ!Xº[‡ïü§GÙ¾“M}n}ð]8mª9pézád%ëÃé
+!É#Îf|ÎX´`ª”íÉfóB½µÁKµœÃD%'“Ís¡¾¯öK¬÷¯CäÓ�š5\…™<a5E£Dp‹DÕdúéÈç-=nŒ
ºoKkø=ʽ§ÿn~þ6iM

+ 1 - 1
dulwich/tests/test_index.py

@@ -4,7 +4,7 @@
 # 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) any later version of the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 1 - 1
dulwich/tests/test_object_store.py

@@ -4,7 +4,7 @@
 # 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) any later version of the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of

+ 16 - 1
dulwich/tests/test_objects.py

@@ -4,7 +4,8 @@
 # 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.
+# of the License or (at your option) any later version of 
+# the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -22,12 +23,14 @@ import unittest
 from dulwich.objects import (Blob,
                          Tree,
                          Commit,
+                         Tag
                          )
 
 a_sha = '6f670c0fb53f9463760b7295fbb814e965fb20c8'
 b_sha = '2969be3e8ee1c0222396a5611407e4769f14e54b'
 c_sha = '954a536f7819d40e6f637f849ee187dd10066349'
 tree_sha = '70c190eb48fa8bbb50ddc692a17b44cb781af7f6'
+tag_sha = '71033db03a03c6a36721efcf1968dd8f8e0cf023'
 
 class BlobReadTests(unittest.TestCase):
   """Test decompression of blobs"""
@@ -43,6 +46,9 @@ class BlobReadTests(unittest.TestCase):
   def get_tree(self, sha):
     return self.get_sha_file(Tree, 'trees', sha)
 
+  def get_tag(self, sha):
+    return self.get_sha_file(Tag, 'tags', sha)
+
   def commit(self, sha):
     return self.get_sha_file(Commit, 'commits', sha)
 
@@ -79,6 +85,15 @@ class BlobReadTests(unittest.TestCase):
     self.assertEqual(t.entries()[0], (33188, 'a', a_sha))
     self.assertEqual(t.entries()[1], (33188, 'b', b_sha))
 
+  def test_read_tag_from_file(self):
+    t = self.get_tag(tag_sha)
+    self.assertEqual(t.object, (Commit, '51b668fd5bf7061b7d6fa525f88803e6cfadaa51'))
+    self.assertEqual(t.name,'signed')
+    self.assertEqual(t.tagger,'Ali Sabil <ali.sabil@gmail.com>')
+    self.assertEqual(t.tag_time, 1231203091)
+    self.assertEqual(t.message, 'This is a signed tag\n-----BEGIN PGP SIGNATURE-----\nVersion: GnuPG v1.4.9 (GNU/Linux)\n\niEYEABECAAYFAkliqx8ACgkQqSMmLy9u/kcx5ACfakZ9NnPl02tOyYP6pkBoEkU1\n5EcAn0UFgokaSvS371Ym/4W9iJj6vh3h\n=ql7y\n-----END PGP SIGNATURE-----\n')
+
+
   def test_read_commit_from_file(self):
     sha = '60dacdc733de308bb77bb76ce0fb0f9b44c9769e'
     c = self.commit(sha)

+ 1 - 1
dulwich/tests/test_pack.py

@@ -190,7 +190,7 @@ class TestPack(PackTests):
 
     def test_copy(self):
         p = self.get_pack(pack1_sha)
-        write_pack("Elch", p.iterobjects(), len(p))
+        write_pack("Elch", [(x, "") for x in p.iterobjects()], len(p))
         self.assertEquals(p, Pack("Elch"))
 
     def test_commit_obj(self):

+ 5 - 1
dulwich/tests/test_repository.py

@@ -4,7 +4,8 @@
 # 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.
+# of the License or (at your option) any later version of 
+# the License.
 # 
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -127,3 +128,6 @@ class RepositoryTests(unittest.TestCase):
                             'fb5b0425c7ce46959bec94d54b9a157645e114f5',
                             'f9e39b120c68182a4ba35349f832d0e4e61f485c'])
 
+  def test_get_tags_empty(self):
+   r = self.open_repo('ooo_merge')
+   self.assertEquals({}, r.get_tags())

+ 4 - 2
setup.py

@@ -4,12 +4,14 @@
 
 from distutils.core import setup
 
+dulwich_version_string = '0.1.0'
+
 setup(name='dulwich',
       description='Pure-Python Git Library',
       keywords='git',
-      version='0.0.1',
+      version=dulwich_version_string,
       url='http://launchpad.net/dulwich',
-      download_url='http://launchpad.net/dulwich',
+      download_url='http://samba.org/~jelmer/dulwich/dulwich-%s.tar.gz' % dulwich_version_string,
       license='GPL',
       author='Jelmer Vernooij',
       author_email='jelmer@samba.org',