瀏覽代碼

Add support for getting the revision graph from a head.

Ask a repo for the revision_history from a commit sha, and it will oblige by 
returning a list of commits that are reverse chronologically sorted.

Doing this required storing the commit time. The author time and the timezones
are currently discarded, but could be parsed.

Also ShaFile now implements __eq__ so that two objects are the same if their
sha value matches.
James Westby 19 年之前
父節點
當前提交
b26531f49d

+ 33 - 6
git/objects.py

@@ -46,10 +46,7 @@ def sha_to_hex(sha):
   """Takes a string and returns the hex of the sha within"""
   """Takes a string and returns the hex of the sha within"""
   hexsha = ''
   hexsha = ''
   for c in sha:
   for c in sha:
-    if ord(c) < 16:
-      hexsha += "0%x" % ord(c)
-    else:
-      hexsha += "%x" % ord(c)
+    hexsha += "%02x" % ord(c)
   assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
   assert len(hexsha) == 40, "Incorrect length of sha1 string: %d" % \
          len(hexsha)
          len(hexsha)
   return hexsha
   return hexsha
@@ -149,6 +146,14 @@ class ShaFile(object):
     ressha.update(self._text)
     ressha.update(self._text)
     return ressha
     return ressha
 
 
+  def __eq__(self, other):
+    """Return true id the sha of the two objects match.
+
+    The __le__ etc methods aren't overriden as they make no sense,
+    certainly at this level.
+    """
+    return self.sha().digest() == other.sha().digest()
+
 class Blob(ShaFile):
 class Blob(ShaFile):
   """A Git Blob object."""
   """A Git Blob object."""
 
 
@@ -259,9 +264,14 @@ class Commit(ShaFile):
            "%s must be followed by space not %s" % (author_id, text[count])
            "%s must be followed by space not %s" % (author_id, text[count])
       count += 1
       count += 1
       self._author = ''
       self._author = ''
-      while text[count] != '\n':
+      while text[count] != '>':
+        assert text[count] != '\n', "Malformed author information"
         self._author += text[count]
         self._author += text[count]
         count += 1
         count += 1
+      self._author += text[count]
+      count += 1
+      while text[count] != '\n':
+        count += 1
       count += 1
       count += 1
     self._committer = None
     self._committer = None
     if text[count:].startswith(committer_id):
     if text[count:].startswith(committer_id):
@@ -270,12 +280,22 @@ class Commit(ShaFile):
            "%s must be followed by space not %s" % (committer_id, text[count])
            "%s must be followed by space not %s" % (committer_id, text[count])
       count += 1
       count += 1
       self._committer = ''
       self._committer = ''
-      while text[count] != '\n':
+      while text[count] != '>':
+        assert text[count] != '\n', "Malformed committer information"
         self._committer += text[count]
         self._committer += text[count]
         count += 1
         count += 1
+      self._committer += text[count]
+      count += 1
+      assert text[count] == ' ', "Invalid commit object, " \
+           "commiter information must be followed by space not %s" % text[count]
+      count += 1
+      self._commit_time = int(text[count:count+10])
+      while text[count] != '\n':
+        count += 1
       count += 1
       count += 1
     assert text[count] == '\n', "There must be a new line after the headers"
     assert text[count] == '\n', "There must be a new line after the headers"
     count += 1
     count += 1
+    # XXX: There can be an encoding field.
     self._message = text[count:]
     self._message = text[count:]
 
 
   def tree(self):
   def tree(self):
@@ -298,6 +318,13 @@ class Commit(ShaFile):
     """Returns the commit message"""
     """Returns the commit message"""
     return self._message
     return self._message
 
 
+  def commit_time(self):
+    """Returns the timestamp of the commit.
+    
+    Returns it as the number of seconds since the epoch.
+    """
+    return self._commit_time
+
 type_map = {
 type_map = {
   blob_id : Blob,
   blob_id : Blob,
   tree_id : Tree,
   tree_id : Tree,

+ 31 - 0
git/repository.py

@@ -84,3 +84,34 @@ class Repository(object):
   def get_blob(self, sha):
   def get_blob(self, sha):
     return self._get_object(sha, Blob)
     return self._get_object(sha, Blob)
 
 
+  def revision_history(self, head):
+    """Returns a list of the commits reachable from head.
+
+    Returns a list of commit objects. the first of which will be the commit
+    of head, then following theat will be the parents.
+
+    Raises NotCommitError if any no commits are referenced, including if the
+    head parameter isn't the sha of a commit.
+
+    XXX: work out how to handle merges.
+    """
+    commit = self.get_commit(head)
+    history = [commit]
+    parents = commit.parents()
+    parent_commits = [[]] * len(parents)
+    i = 0
+    for parent in parents:
+      parent_commits[i] = self.revision_history(parent)
+      i += 1
+    for commit_list in parent_commits:
+      for parent_commit in commit_list:
+        if parent_commit in history:
+          continue
+        j = 0
+        for main_commit in history:
+          if main_commit.commit_time() < parent_commit.commit_time():
+            break
+          j += 1
+        history.insert(j, parent_commit)
+    return history
+

+ 1 - 0
git/tests/data/repos/simple_merge/a

@@ -0,0 +1 @@
+test 1

+ 1 - 0
git/tests/data/repos/simple_merge/b

@@ -0,0 +1 @@
+test 2

+ 0 - 0
git/tests/data/repos/simple_merge/d


+ 0 - 0
git/tests/data/repos/simple_merge/e


+ 14 - 6
git/tests/test_objects.py

@@ -69,6 +69,11 @@ class BlobReadTests(unittest.TestCase):
     self.assertEqual(b.text(), string)
     self.assertEqual(b.text(), string)
     self.assertEqual(b.sha().hexdigest(), c_sha)
     self.assertEqual(b.sha().hexdigest(), c_sha)
 
 
+  def test_eq(self):
+    blob1 = self.get_blob(a_sha)
+    blob2 = self.get_blob(a_sha)
+    self.assertEqual(blob1, blob2)
+
   def test_read_tree_from_file(self):
   def test_read_tree_from_file(self):
     t = self.get_tree(tree_sha)
     t = self.get_tree(tree_sha)
     self.assertEqual(t.entries()[0], (33188, 'a', a_sha))
     self.assertEqual(t.entries()[0], (33188, 'a', a_sha))
@@ -80,9 +85,10 @@ class BlobReadTests(unittest.TestCase):
     self.assertEqual(c.tree(), tree_sha)
     self.assertEqual(c.tree(), tree_sha)
     self.assertEqual(c.parents(), ['0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
     self.assertEqual(c.parents(), ['0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
     self.assertEqual(c.author(),
     self.assertEqual(c.author(),
-        'James Westby <jw+debian@jameswestby.net> 1174759230 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
     self.assertEqual(c.committer(),
     self.assertEqual(c.committer(),
-        'James Westby <jw+debian@jameswestby.net> 1174759230 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
+    self.assertEqual(c.commit_time(), 1174759230)
     self.assertEqual(c.message(), 'Test commit\n')
     self.assertEqual(c.message(), 'Test commit\n')
 
 
   def test_read_commit_no_parents(self):
   def test_read_commit_no_parents(self):
@@ -91,9 +97,10 @@ class BlobReadTests(unittest.TestCase):
     self.assertEqual(c.tree(), '90182552c4a85a45ec2a835cadc3451bebdfe870')
     self.assertEqual(c.tree(), '90182552c4a85a45ec2a835cadc3451bebdfe870')
     self.assertEqual(c.parents(), [])
     self.assertEqual(c.parents(), [])
     self.assertEqual(c.author(),
     self.assertEqual(c.author(),
-        'James Westby <jw+debian@jameswestby.net> 1174758034 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
     self.assertEqual(c.committer(),
     self.assertEqual(c.committer(),
-        'James Westby <jw+debian@jameswestby.net> 1174758034 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
+    self.assertEqual(c.commit_time(), 1174758034)
     self.assertEqual(c.message(), 'Test commit\n')
     self.assertEqual(c.message(), 'Test commit\n')
 
 
   def test_read_commit_two_parents(self):
   def test_read_commit_two_parents(self):
@@ -103,8 +110,9 @@ class BlobReadTests(unittest.TestCase):
     self.assertEqual(c.parents(), ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
     self.assertEqual(c.parents(), ['ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
                                    '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'])
                                    '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6'])
     self.assertEqual(c.author(),
     self.assertEqual(c.author(),
-        'James Westby <jw+debian@jameswestby.net> 1174773719 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
     self.assertEqual(c.committer(),
     self.assertEqual(c.committer(),
-        'James Westby <jw+debian@jameswestby.net> 1174773719 +0000')
+        'James Westby <jw+debian@jameswestby.net>')
+    self.assertEqual(c.commit_time(), 1174773719)
     self.assertEqual(c.message(), 'Merge ../b\n')
     self.assertEqual(c.message(), 'Merge ../b\n')
 
 

+ 18 - 0
git/tests/test_repository.py

@@ -90,3 +90,21 @@ class RepositoryTests(unittest.TestCase):
     r = self.open_repo('a')
     r = self.open_repo('a')
     self.assertRaises(NotBlobError, r.get_blob, r.head())
     self.assertRaises(NotBlobError, r.get_blob, r.head())
 
 
+  def test_linear_history(self):
+    r = self.open_repo('a')
+    history = r.revision_history(r.head())
+    shas = [c.sha().hexdigest() for c in history]
+    self.assertEqual(shas, [r.head(),
+                            '2a72d929692c41d8554c07f6301757ba18a65d91'])
+
+  def test_merge_history(self):
+    r = self.open_repo('simple_merge')
+    history = r.revision_history(r.head())
+    shas = [c.sha().hexdigest() for c in history]
+    self.assertEqual(shas, ['5dac377bdded4c9aeb8dff595f0faeebcc8498cc',
+                            'ab64bbdcc51b170d21588e5c5d391ee5c0c96dfd',
+                            '4cffe90e0a41ad3f5190079d7c8f036bde29cbe6',
+                            '60dacdc733de308bb77bb76ce0fb0f9b44c9769e',
+                            '0d89f20333fbb1d2f3a94da77f4981373d8f4310'])
+
+