"""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))
"""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):
: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()))
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):
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()
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):
"""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):
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.
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())