Fix flags data in index.
[jelmer/dulwich-libgit2.git] / dulwich / index.py
index cacb79387e7d2cd158b880c67187840629d27b6a..c238de11df238c7397963688b01853c2f36cf4a8 100644 (file)
@@ -1,5 +1,5 @@
 # index.py -- File parser/write for the git index file
-# Copryight (C) 2008 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
 # modify it under the terms of the GNU General Public License
 
 """Parser for the git index file format."""
 
+import os
+import stat
 import struct
 
+from dulwich.objects import (
+    Tree,
+    hex_to_sha,
+    sha_to_hex,
+    )
+from dulwich.pack import (
+    SHA1Reader,
+    SHA1Writer,
+    )
+
+
 def read_cache_time(f):
+    """Read a cache time."""
     return struct.unpack(">LL", f.read(8))
 
 
 def write_cache_time(f, t):
+    """Write a cache time."""
+    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))
 
 
 def read_cache_entry(f):
+    """Read an entry from a cache file.
+
+    :param f: File-like object to read from
+    :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, 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)
 
 
 def write_cache_entry(f, entry):
@@ -50,18 +74,18 @@ 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, sha, flags))
+    flags = len(name) & flags
+    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 + 8) & ~7)
-    f.write(chr(0) * (f.tell() - (beginoffset + real_size)))
-    return 
+    f.write("\0" * ((beginoffset + real_size) - f.tell()))
+
 
 def read_index(f):
     """Read an index file, yielding the individual entries."""
@@ -80,8 +104,8 @@ def read_index_dict(f):
     :param f: File object to read from
     """
     ret = {}
-    for (name, ctime, mtime, ino, dev, mode, uid, gid, size, sha, flags) in read_index(f):
-        ret[name] = (ctime, mtime, ino, dev, mode, uid, gid, size, sha, flags)
+    for x in read_index(f):
+        ret[x[0]] = tuple(x[1:])
     return ret
 
 
@@ -107,45 +131,123 @@ def write_index_dict(f, entries):
     write_index(f, entries_list)
 
 
+def cleanup_mode(mode):
+    if stat.S_ISLNK(fsmode):
+        mode = stat.S_IFLNK
+    else:
+        mode = stat.S_IFREG
+    mode |= (fsmode & 0111)
+    return mode
+
+
 class Index(object):
+    """A Git Index file."""
 
     def __init__(self, filename):
-        self._entries = []
+        """Open an index file.
+        
+        :param filename: Path to the index file
+        """
         self._filename = filename
-        self._byname = {}
+        self.clear()
         self.read()
 
     def write(self):
-        f = open(self._filename, 'w')
+        """Write current contents of index to disk."""
+        f = open(self._filename, 'wb')
         try:
-            write_index(f, self._entries)
+            f = SHA1Writer(f)
+            write_index_dict(f, self._byname)
         finally:
             f.close()
 
     def read(self):
-        f = open(self._filename, 'r')
+        """Read current contents of index from disk."""
+        f = open(self._filename, 'rb')
         try:
+            f = SHA1Reader(f)
             for x in read_index(f):
-                self[x[0]] = x
+                self[x[0]] = tuple(x[1:])
+            f.check_sha()
         finally:
             f.close()
 
     def __len__(self):
-        return len(self._entries)
+        """Number of entries in this index file."""
+        return len(self._byname)
 
-    def items(self):
-        return list(self._entries)
+    def __getitem__(self, name):
+        """Retrieve entry by relative path."""
+        return self._byname[name]
 
     def __iter__(self):
-        return iter(self._entries)
+        """Iterate over the paths in this index."""
+        return iter(self._byname)
 
-    def __getitem__(self, name):
-        return self._byname[name]
+    def get_sha1(self, path):
+        """Return the (git object) SHA1 for the object at a path."""
+        return self[path][-2]
+
+    def iterblobs(self):
+        """Iterate over path, sha, mode tuples for use with commit_tree."""
+        for path, entry in self:
+            yield path, entry[-2], cleanup_mode(entry[-6])
+
+    def clear(self):
+        """Remove all contents from this index."""
+        self._byname = {}
 
     def __setitem__(self, name, x):
+        assert isinstance(name, str)
+        assert len(x) == 10
         # Remove the old entry if any
-        old_entry = self._byname.get(x[0])
-        if old_entry is not None:
-            self._entries.remove(old_entry)
-        self._entries.append(x)
-        self._byname[x[0]] = x
+        self._byname[name] = x
+
+    def iteritems(self):
+        return self._byname.iteritems()
+
+    def update(self, entries):
+        for name, value in entries.iteritems():
+            self[name] = value
+
+
+def commit_tree(object_store, blobs):
+    """Commit a new tree.
+
+    :param object_store: Object store to add trees to
+    :param blobs: Iterable over blob path, sha, mode entries
+    :return: SHA1 of the created tree.
+    """
+    trees = {"": {}}
+    def add_tree(path):
+        if path in trees:
+            return trees[path]
+        dirname, basename = os.path.split(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 = add_tree(tree_path)
+        tree[basename] = (mode, sha)
+
+    def build_tree(path):
+        tree = Tree()
+        for basename, entry in trees[path].iteritems():
+            if type(entry) == dict:
+                mode = stat.S_IFDIR
+                sha = build_tree(os.path.join(path, basename))
+            else:
+                (mode, sha) = entry
+            tree.add(mode, basename, sha)
+        object_store.add_object(tree)
+        return tree.id
+    return build_tree("")
+
+
+def commit_index(object_store, index):
+    return commit_tree(object_store, index.blobs())