Support progress argument to generate_pack_contents.
[jelmer/dulwich-libgit2.git] / dulwich / index.py
index dce305d962d90de38be89fb06e82fb8e33f57c73..b2c2619e86d41358f128568cdc982d01909a4fc4 100644 (file)
 
 """Parser for the git index file format."""
 
+import os
 import stat
 import struct
 
+from dulwich.file import GitFile
 from dulwich.objects import (
+    S_IFGITLINK,
+    S_ISGITLINK,
     Tree,
     hex_to_sha,
     sha_to_hex,
     )
+from dulwich.pack import (
+    SHA1Reader,
+    SHA1Writer,
+    )
+
+
+def pathsplit(path):
+    """Split a /-delimited path into a directory part and a basename.
+
+    :param path: The path to split.
+    :return: Tuple with directory name and basename
+    """
+    try:
+        (dirname, basename) = path.rsplit("/", 1)
+    except ValueError:
+        return ("", path)
+    else:
+        return (dirname, basename)
+
+
+def pathjoin(*args):
+    """Join a /-delimited path.
+
+    """
+    return "/".join([p for p in args if p])
 
 
 def read_cache_time(f):
-    """Read a cache time."""
+    """Read a cache time.
+    
+    :param f: File-like object to read from
+    :return: Tuple with seconds and nanoseconds
+    """
     return struct.unpack(">LL", f.read(8))
 
 
 def write_cache_time(f, t):
-    """Write a cache time."""
+    """Write a cache time.
+    
+    :param f: File-like object to write to
+    :param t: Time to write (as int, float or tuple with secs and nsecs)
+    """
     if isinstance(t, int):
         t = (t, 0)
+    elif isinstance(t, float):
+        (secs, nsecs) = divmod(t, 1.0)
+        t = (int(secs), int(nsecs * 1000000000))
+    elif not isinstance(t, tuple):
+        raise TypeError(t)
     f.write(struct.pack(">LL", *t))
 
 
@@ -44,23 +86,19 @@ def read_cache_entry(f):
     """Read an entry from a cache file.
 
     :param f: File-like object to read from
-    :return: tuple with: inode, device, mode, uid, gid, size, sha, flags
+    :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
     """
     beginoffset = f.tell()
     ctime = read_cache_time(f)
     mtime = read_cache_time(f)
-    (ino, dev, mode, uid, gid, size, sha, flags, ) = \
+    (dev, ino, mode, uid, gid, size, sha, flags, ) = \
         struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
-    name = ""
-    char = f.read(1)
-    while char != "\0":
-        name += char
-        char = f.read(1)
+    name = f.read((flags & 0x0fff))
     # Padding:
-    real_size = ((f.tell() - beginoffset + 7) & ~7)
-    f.seek(beginoffset + real_size)
-    return (name, ctime, mtime, ino, dev, mode, uid, gid, size, 
-            sha_to_hex(sha), flags)
+    real_size = ((f.tell() - beginoffset + 8) & ~7)
+    data = f.read((beginoffset + real_size) - f.tell())
+    return (name, ctime, mtime, dev, ino, mode, uid, gid, size, 
+            sha_to_hex(sha), flags & ~0x0fff)
 
 
 def write_cache_entry(f, entry):
@@ -68,16 +106,16 @@ def write_cache_entry(f, entry):
 
     :param f: File object
     :param entry: Entry to write, tuple with: 
-        (name, ctime, mtime, ino, dev, mode, uid, gid, size, sha, flags)
+        (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
     """
     beginoffset = f.tell()
-    (name, ctime, mtime, ino, dev, mode, uid, gid, size, sha, flags) = entry
+    (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
     write_cache_time(f, ctime)
     write_cache_time(f, mtime)
-    f.write(struct.pack(">LLLLLL20sH", ino, dev, mode, uid, gid, size, hex_to_sha(sha), flags))
+    flags = len(name) | (flags &~ 0x0fff)
+    f.write(struct.pack(">LLLLLL20sH", dev, ino, mode, uid, gid, size, hex_to_sha(sha), flags))
     f.write(name)
-    f.write(chr(0))
-    real_size = ((f.tell() - beginoffset + 7) & ~7)
+    real_size = ((f.tell() - beginoffset + 8) & ~7)
     f.write("\0" * ((beginoffset + real_size) - f.tell()))
 
 
@@ -126,12 +164,21 @@ def write_index_dict(f, entries):
 
 
 def cleanup_mode(mode):
-    if stat.S_ISLNK(fsmode):
-        mode = stat.S_IFLNK
-    else:
-        mode = stat.S_IFREG
-    mode |= (fsmode & 0111)
-    return mode
+    """Cleanup a mode value.
+
+    This will return a mode that can be stored in a tree object.
+    
+    :param mode: Mode to clean up.
+    """
+    if stat.S_ISLNK(mode):
+        return stat.S_IFLNK
+    elif stat.S_ISDIR(mode):
+        return stat.S_IFDIR
+    elif S_ISGITLINK(mode):
+        return S_IFGITLINK
+    ret = stat.S_IFREG | 0644
+    ret |= (mode & 0111)
+    return ret
 
 
 class Index(object):
@@ -148,18 +195,23 @@ class Index(object):
 
     def write(self):
         """Write current contents of index to disk."""
-        f = open(self._filename, 'w')
+        f = GitFile(self._filename, 'wb')
         try:
+            f = SHA1Writer(f)
             write_index_dict(f, self._byname)
         finally:
             f.close()
 
     def read(self):
         """Read current contents of index from disk."""
-        f = open(self._filename, 'r')
+        f = GitFile(self._filename, 'rb')
         try:
+            f = SHA1Reader(f)
             for x in read_index(f):
                 self[x[0]] = tuple(x[1:])
+            # FIXME: Additional data?
+            f.read(os.path.getsize(self._filename)-f.tell()-20)
+            f.check_sha()
         finally:
             f.close()
 
@@ -168,7 +220,10 @@ class Index(object):
         return len(self._byname)
 
     def __getitem__(self, name):
-        """Retrieve entry by relative path."""
+        """Retrieve entry by relative path.
+        
+        :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
+        """
         return self._byname[name]
 
     def __iter__(self):
@@ -179,9 +234,14 @@ class Index(object):
         """Return the (git object) SHA1 for the object at a path."""
         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):
         """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])
 
     def clear(self):
@@ -201,6 +261,36 @@ class Index(object):
         for name, value in entries.iteritems():
             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(self, object_store):
+        """Create a new tree from an index.
+
+        :param object_store: Object store to save the tree in
+        :return: Root tree SHA
+        """
+        return commit_tree(object_store, self.iterblobs())
+
 
 def commit_tree(object_store, blobs):
     """Commit a new tree.
@@ -213,30 +303,39 @@ def commit_tree(object_store, blobs):
     def add_tree(path):
         if path in trees:
             return trees[path]
-        dirname, basename = os.path.split(path)
+        dirname, basename = pathsplit(path)
         t = add_tree(dirname)
         assert isinstance(basename, str)
         newtree = {}
         t[basename] = newtree
+        trees[path] = newtree
         return newtree
 
     for path, sha, mode in blobs:
-        tree_path, basename = os.path.split(path)
+        tree_path, basename = pathsplit(path)
         tree = add_tree(tree_path)
         tree[basename] = (mode, sha)
 
-    for path in sorted(trees.keys(), reverse=True):
+    def build_tree(path):
         tree = Tree()
-        for basename, (mode, sha) in trees[path]:
+        for basename, entry in trees[path].iteritems():
+            if type(entry) == dict:
+                mode = stat.S_IFDIR
+                sha = build_tree(pathjoin(path, basename))
+            else:
+                (mode, sha) = entry
             tree.add(mode, basename, sha)
-        if path != "":
-            # Add to object store
-            parent_path, basename = os.path.split(path)
-            # Update sha in parent
-            trees[parent_path][basename] = (stat.S_IFDIR, tree.id)
-        else:
-            return tree.id
+        object_store.add_object(tree)
+        return tree.id
+    return build_tree("")
 
 
 def commit_index(object_store, index):
-    return commit_tree(object_store, index.blobs())
+    """Create a new tree from an index.
+
+    :param object_store: Object store to save the tree in
+    :param index: Index file
+    :note: This function is deprecated, use index.commit() instead.
+    :return: Root tree sha.
+    """
+    return commit_tree(object_store, index.iterblobs())