1 # index.py -- File parser/writer for the git index file
2 # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; version 2
7 # of the License or (at your opinion) any later version of the license.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19 """Parser for the git index file format."""
26 from dulwich.file import GitFile
27 from dulwich.objects import (
34 from dulwich.pack import (
41 """Split a /-delimited path into a directory part and a basename.
43 :param path: The path to split.
44 :return: Tuple with directory name and basename
47 (dirname, basename) = path.rsplit("/", 1)
51 return (dirname, basename)
55 """Join a /-delimited path.
58 return "/".join([p for p in args if p])
61 def read_cache_time(f):
64 :param f: File-like object to read from
65 :return: Tuple with seconds and nanoseconds
67 return struct.unpack(">LL", f.read(8))
70 def write_cache_time(f, t):
71 """Write a cache time.
73 :param f: File-like object to write to
74 :param t: Time to write (as int, float or tuple with secs and nsecs)
76 if isinstance(t, int):
78 elif isinstance(t, float):
79 (secs, nsecs) = divmod(t, 1.0)
80 t = (int(secs), int(nsecs * 1000000000))
81 elif not isinstance(t, tuple):
83 f.write(struct.pack(">LL", *t))
86 def read_cache_entry(f):
87 """Read an entry from a cache file.
89 :param f: File-like object to read from
90 :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
92 beginoffset = f.tell()
93 ctime = read_cache_time(f)
94 mtime = read_cache_time(f)
95 (dev, ino, mode, uid, gid, size, sha, flags, ) = \
96 struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
97 name = f.read((flags & 0x0fff))
99 real_size = ((f.tell() - beginoffset + 8) & ~7)
100 data = f.read((beginoffset + real_size) - f.tell())
101 return (name, ctime, mtime, dev, ino, mode, uid, gid, size,
102 sha_to_hex(sha), flags & ~0x0fff)
105 def write_cache_entry(f, entry):
106 """Write an index entry to a file.
108 :param f: File object
109 :param entry: Entry to write, tuple with:
110 (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
112 beginoffset = f.tell()
113 (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
114 write_cache_time(f, ctime)
115 write_cache_time(f, mtime)
116 flags = len(name) | (flags &~ 0x0fff)
117 f.write(struct.pack(">LLLLLL20sH", dev & 0xFFFFFFFF, ino & 0xFFFFFFFF, mode, uid, gid, size, hex_to_sha(sha), flags))
119 real_size = ((f.tell() - beginoffset + 8) & ~7)
120 f.write("\0" * ((beginoffset + real_size) - f.tell()))
124 """Read an index file, yielding the individual entries."""
127 raise AssertionError("Invalid index file header: %r" % header)
128 (version, num_entries) = struct.unpack(">LL", f.read(4 * 2))
129 assert version in (1, 2)
130 for i in range(num_entries):
131 yield read_cache_entry(f)
134 def read_index_dict(f):
135 """Read an index file and return it as a dictionary.
137 :param f: File object to read from
140 for x in read_index(f):
141 ret[x[0]] = tuple(x[1:])
145 def write_index(f, entries):
146 """Write an index file.
148 :param f: File-like object to write to
149 :param entries: Iterable over the entries to write
152 f.write(struct.pack(">LL", 2, len(entries)))
154 write_cache_entry(f, x)
157 def write_index_dict(f, entries):
158 """Write an index file based on the contents of a dictionary.
162 for name in sorted(entries):
163 entries_list.append((name,) + tuple(entries[name]))
164 write_index(f, entries_list)
167 def cleanup_mode(mode):
168 """Cleanup a mode value.
170 This will return a mode that can be stored in a tree object.
172 :param mode: Mode to clean up.
174 if stat.S_ISLNK(mode):
176 elif stat.S_ISDIR(mode):
178 elif S_ISGITLINK(mode):
180 ret = stat.S_IFREG | 0644
186 """A Git Index file."""
188 def __init__(self, filename):
189 """Open an index file.
191 :param filename: Path to the index file
193 self._filename = filename
198 return "%s(%r)" % (self.__class__.__name__, self._filename)
201 """Write current contents of index to disk."""
202 f = GitFile(self._filename, 'wb')
205 write_index_dict(f, self._byname)
210 """Read current contents of index from disk."""
211 if not os.path.exists(self._filename):
213 f = GitFile(self._filename, 'rb')
216 for x in read_index(f):
217 self[x[0]] = tuple(x[1:])
218 # FIXME: Additional data?
219 f.read(os.path.getsize(self._filename)-f.tell()-20)
225 """Number of entries in this index file."""
226 return len(self._byname)
228 def __getitem__(self, name):
229 """Retrieve entry by relative path.
231 :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
233 return self._byname[name]
236 """Iterate over the paths in this index."""
237 return iter(self._byname)
239 def get_sha1(self, path):
240 """Return the (git object) SHA1 for the object at a path."""
241 return self[path][-2]
243 def get_mode(self, path):
244 """Return the POSIX file mode for the object at a path."""
245 return self[path][-6]
248 """Iterate over path, sha, mode tuples for use with commit_tree."""
251 yield path, entry[-2], cleanup_mode(entry[-6])
254 """Remove all contents from this index."""
257 def __setitem__(self, name, x):
258 assert isinstance(name, str)
260 # Remove the old entry if any
261 self._byname[name] = x
263 def __delitem__(self, name):
264 assert isinstance(name, str)
265 del self._byname[name]
268 return self._byname.iteritems()
270 def update(self, entries):
271 for name, value in entries.iteritems():
274 def changes_from_tree(self, object_store, tree, want_unchanged=False):
275 """Find the differences between the contents of this index and a tree.
277 :param object_store: Object store to use for retrieving tree contents
278 :param tree: SHA1 of the root tree
279 :param want_unchanged: Whether unchanged files should be reported
280 :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
282 def lookup_entry(path):
284 return entry[-2], entry[-6]
285 for (name, mode, sha) in changes_from_tree(self._byname.keys(),
286 lookup_entry, object_store, tree,
287 want_unchanged=want_unchanged):
288 yield (name, mode, sha)
290 def commit(self, object_store):
291 """Create a new tree from an index.
293 :param object_store: Object store to save the tree in
294 :return: Root tree SHA
296 return commit_tree(object_store, self.iterblobs())
299 def commit_tree(object_store, blobs):
300 """Commit a new tree.
302 :param object_store: Object store to add trees to
303 :param blobs: Iterable over blob path, sha, mode entries
304 :return: SHA1 of the created tree.
312 dirname, basename = pathsplit(path)
313 t = add_tree(dirname)
314 assert isinstance(basename, str)
316 t[basename] = newtree
317 trees[path] = newtree
320 for path, sha, mode in blobs:
321 tree_path, basename = pathsplit(path)
322 tree = add_tree(tree_path)
323 tree[basename] = (mode, sha)
325 def build_tree(path):
327 for basename, entry in trees[path].iteritems():
328 if type(entry) == dict:
330 sha = build_tree(pathjoin(path, basename))
333 tree.add(basename, mode, sha)
334 object_store.add_object(tree)
336 return build_tree("")
339 def commit_index(object_store, index):
340 """Create a new tree from an index.
342 :param object_store: Object store to save the tree in
343 :param index: Index file
344 :note: This function is deprecated, use index.commit() instead.
345 :return: Root tree sha.
347 return commit_tree(object_store, index.iterblobs())
350 def changes_from_tree(names, lookup_entry, object_store, tree,
351 want_unchanged=False):
352 """Find the differences between the contents of a tree and
355 :param names: Iterable of names in the working copy
356 :param lookup_entry: Function to lookup an entry in the working copy
357 :param object_store: Object store to use for retrieving tree contents
358 :param tree: SHA1 of the root tree, or None for an empty tree
359 :param want_unchanged: Whether unchanged files should be reported
360 :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode),
363 other_names = set(names)
366 for (name, mode, sha) in object_store.iter_tree_contents(tree):
368 (other_sha, other_mode) = lookup_entry(name)
371 yield ((name, None), (mode, None), (sha, None))
373 other_names.remove(name)
374 if (want_unchanged or other_sha != sha or other_mode != mode):
375 yield ((name, name), (mode, other_mode), (sha, other_sha))
377 # Mention added files
378 for name in other_names:
379 (other_sha, other_mode) = lookup_entry(name)
380 yield ((None, name), (None, other_mode), (None, other_sha))
383 def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
384 """Create a new index entry from a stat value.
386 :param stat_val: POSIX stat_result instance
387 :param hex_sha: Hex sha of the object
388 :param flags: Index flags
391 mode = cleanup_mode(stat_val.st_mode)
392 return (stat_val.st_ctime, stat_val.st_mtime, stat_val.st_dev,
393 stat_val.st_ino, mode, stat_val.st_uid,
394 stat_val.st_gid, stat_val.st_size, hex_sha, flags)
397 def build_index_from_tree(prefix, index_path, object_store, tree_id,
398 honor_filemode=True):
399 """Generate and materialize index from a tree
401 :param tree_id: Tree to materialize
402 :param prefix: Target dir for materialized index files
403 :param index_path: Target path for generated index
404 :param object_store: Non-empty object store holding tree contents
405 :param honor_filemode: An optional flag to honor core.filemode setting in
406 config file, default is core.filemode=True, change executable bit
408 :note:: existing index is wiped and contents are not merged
409 in a working dir. Suiteable only for fresh clones.
412 index = Index(index_path)
414 for entry in object_store.iter_tree_contents(tree_id):
415 full_path = os.path.join(prefix, entry.path)
417 if not os.path.exists(os.path.dirname(full_path)):
418 os.makedirs(os.path.dirname(full_path))
420 # FIXME: Merge new index into working tree
421 if stat.S_ISLNK(entry.mode):
422 # FIXME: This will fail on Windows. What should we do instead?
423 src_path = object_store[entry.sha].as_raw_string()
425 os.symlink(src_path, full_path)
427 if e.errno == errno.EEXIST:
429 os.symlink(src_path, full_path)
433 f = open(full_path, 'wb')
436 f.write(object_store[entry.sha].as_raw_string())
441 os.chmod(full_path, entry.mode)
444 st = os.lstat(full_path)
445 index[entry.path] = index_entry_from_stat(st, entry.sha, 0)