Avoid relying on ~/.gitconfig existing.
[jelmer/dulwich.git] / dulwich / index.py
1 # index.py -- File parser/writer for the git index file
2 # Copyright (C) 2008-2013 Jelmer Vernooij <jelmer@samba.org>
3 #
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.
8 #
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.
13 #
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,
17 # MA  02110-1301, USA.
18
19 """Parser for the git index file format."""
20
21 import errno
22 import os
23 import stat
24 import struct
25
26 from dulwich.file import GitFile
27 from dulwich.objects import (
28     S_IFGITLINK,
29     S_ISGITLINK,
30     Tree,
31     hex_to_sha,
32     sha_to_hex,
33     )
34 from dulwich.pack import (
35     SHA1Reader,
36     SHA1Writer,
37     )
38
39
40 def pathsplit(path):
41     """Split a /-delimited path into a directory part and a basename.
42
43     :param path: The path to split.
44     :return: Tuple with directory name and basename
45     """
46     try:
47         (dirname, basename) = path.rsplit("/", 1)
48     except ValueError:
49         return ("", path)
50     else:
51         return (dirname, basename)
52
53
54 def pathjoin(*args):
55     """Join a /-delimited path.
56
57     """
58     return "/".join([p for p in args if p])
59
60
61 def read_cache_time(f):
62     """Read a cache time.
63
64     :param f: File-like object to read from
65     :return: Tuple with seconds and nanoseconds
66     """
67     return struct.unpack(">LL", f.read(8))
68
69
70 def write_cache_time(f, t):
71     """Write a cache time.
72
73     :param f: File-like object to write to
74     :param t: Time to write (as int, float or tuple with secs and nsecs)
75     """
76     if isinstance(t, int):
77         t = (t, 0)
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):
82         raise TypeError(t)
83     f.write(struct.pack(">LL", *t))
84
85
86 def read_cache_entry(f):
87     """Read an entry from a cache file.
88
89     :param f: File-like object to read from
90     :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
91     """
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))
98     # Padding:
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)
103
104
105 def write_cache_entry(f, entry):
106     """Write an index entry to a file.
107
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)
111     """
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))
118     f.write(name)
119     real_size = ((f.tell() - beginoffset + 8) & ~7)
120     f.write("\0" * ((beginoffset + real_size) - f.tell()))
121
122
123 def read_index(f):
124     """Read an index file, yielding the individual entries."""
125     header = f.read(4)
126     if header != "DIRC":
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)
132
133
134 def read_index_dict(f):
135     """Read an index file and return it as a dictionary.
136
137     :param f: File object to read from
138     """
139     ret = {}
140     for x in read_index(f):
141         ret[x[0]] = tuple(x[1:])
142     return ret
143
144
145 def write_index(f, entries):
146     """Write an index file.
147
148     :param f: File-like object to write to
149     :param entries: Iterable over the entries to write
150     """
151     f.write("DIRC")
152     f.write(struct.pack(">LL", 2, len(entries)))
153     for x in entries:
154         write_cache_entry(f, x)
155
156
157 def write_index_dict(f, entries):
158     """Write an index file based on the contents of a dictionary.
159
160     """
161     entries_list = []
162     for name in sorted(entries):
163         entries_list.append((name,) + tuple(entries[name]))
164     write_index(f, entries_list)
165
166
167 def cleanup_mode(mode):
168     """Cleanup a mode value.
169
170     This will return a mode that can be stored in a tree object.
171
172     :param mode: Mode to clean up.
173     """
174     if stat.S_ISLNK(mode):
175         return stat.S_IFLNK
176     elif stat.S_ISDIR(mode):
177         return stat.S_IFDIR
178     elif S_ISGITLINK(mode):
179         return S_IFGITLINK
180     ret = stat.S_IFREG | 0644
181     ret |= (mode & 0111)
182     return ret
183
184
185 class Index(object):
186     """A Git Index file."""
187
188     def __init__(self, filename):
189         """Open an index file.
190
191         :param filename: Path to the index file
192         """
193         self._filename = filename
194         self.clear()
195         self.read()
196
197     def __repr__(self):
198         return "%s(%r)" % (self.__class__.__name__, self._filename)
199
200     def write(self):
201         """Write current contents of index to disk."""
202         f = GitFile(self._filename, 'wb')
203         try:
204             f = SHA1Writer(f)
205             write_index_dict(f, self._byname)
206         finally:
207             f.close()
208
209     def read(self):
210         """Read current contents of index from disk."""
211         if not os.path.exists(self._filename):
212             return
213         f = GitFile(self._filename, 'rb')
214         try:
215             f = SHA1Reader(f)
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)
220             f.check_sha()
221         finally:
222             f.close()
223
224     def __len__(self):
225         """Number of entries in this index file."""
226         return len(self._byname)
227
228     def __getitem__(self, name):
229         """Retrieve entry by relative path.
230
231         :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
232         """
233         return self._byname[name]
234
235     def __iter__(self):
236         """Iterate over the paths in this index."""
237         return iter(self._byname)
238
239     def get_sha1(self, path):
240         """Return the (git object) SHA1 for the object at a path."""
241         return self[path][-2]
242
243     def get_mode(self, path):
244         """Return the POSIX file mode for the object at a path."""
245         return self[path][-6]
246
247     def iterblobs(self):
248         """Iterate over path, sha, mode tuples for use with commit_tree."""
249         for path in self:
250             entry = self[path]
251             yield path, entry[-2], cleanup_mode(entry[-6])
252
253     def clear(self):
254         """Remove all contents from this index."""
255         self._byname = {}
256
257     def __setitem__(self, name, x):
258         assert isinstance(name, str)
259         assert len(x) == 10
260         # Remove the old entry if any
261         self._byname[name] = x
262
263     def __delitem__(self, name):
264         assert isinstance(name, str)
265         del self._byname[name]
266
267     def iteritems(self):
268         return self._byname.iteritems()
269
270     def update(self, entries):
271         for name, value in entries.iteritems():
272             self[name] = value
273
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.
276
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)
281         """
282         def lookup_entry(path):
283             entry = self[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)
289
290     def commit(self, object_store):
291         """Create a new tree from an index.
292
293         :param object_store: Object store to save the tree in
294         :return: Root tree SHA
295         """
296         return commit_tree(object_store, self.iterblobs())
297
298
299 def commit_tree(object_store, blobs):
300     """Commit a new tree.
301
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.
305     """
306
307     trees = {"": {}}
308
309     def add_tree(path):
310         if path in trees:
311             return trees[path]
312         dirname, basename = pathsplit(path)
313         t = add_tree(dirname)
314         assert isinstance(basename, str)
315         newtree = {}
316         t[basename] = newtree
317         trees[path] = newtree
318         return newtree
319
320     for path, sha, mode in blobs:
321         tree_path, basename = pathsplit(path)
322         tree = add_tree(tree_path)
323         tree[basename] = (mode, sha)
324
325     def build_tree(path):
326         tree = Tree()
327         for basename, entry in trees[path].iteritems():
328             if type(entry) == dict:
329                 mode = stat.S_IFDIR
330                 sha = build_tree(pathjoin(path, basename))
331             else:
332                 (mode, sha) = entry
333             tree.add(basename, mode, sha)
334         object_store.add_object(tree)
335         return tree.id
336     return build_tree("")
337
338
339 def commit_index(object_store, index):
340     """Create a new tree from an index.
341
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.
346     """
347     return commit_tree(object_store, index.iterblobs())
348
349
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
353     a working copy.
354
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),
361         (oldsha, newsha)
362     """
363     other_names = set(names)
364
365     if tree is not None:
366         for (name, mode, sha) in object_store.iter_tree_contents(tree):
367             try:
368                 (other_sha, other_mode) = lookup_entry(name)
369             except KeyError:
370                 # Was removed
371                 yield ((name, None), (mode, None), (sha, None))
372             else:
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))
376
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))
381
382
383 def index_entry_from_stat(stat_val, hex_sha, flags, mode=None):
384     """Create a new index entry from a stat value.
385
386     :param stat_val: POSIX stat_result instance
387     :param hex_sha: Hex sha of the object
388     :param flags: Index flags
389     """
390     if mode is None:
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)
395
396
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
400
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
407
408     :note:: existing index is wiped and contents are not merged
409         in a working dir. Suiteable only for fresh clones.
410     """
411
412     index = Index(index_path)
413
414     for entry in object_store.iter_tree_contents(tree_id):
415         full_path = os.path.join(prefix, entry.path)
416
417         if not os.path.exists(os.path.dirname(full_path)):
418             os.makedirs(os.path.dirname(full_path))
419
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()
424             try:
425                 os.symlink(src_path, full_path)
426             except OSError as e:
427                 if e.errno == errno.EEXIST:
428                     os.unlink(full_path)
429                     os.symlink(src_path, full_path)
430                 else:
431                     raise
432         else:
433             f = open(full_path, 'wb')
434             try:
435                 # Write out file
436                 f.write(object_store[entry.sha].as_raw_string())
437             finally:
438                 f.close()
439
440             if honor_filemode:
441                 os.chmod(full_path, entry.mode)
442
443         # Add file to index
444         st = os.lstat(full_path)
445         index[entry.path] = index_entry_from_stat(st, entry.sha, 0)
446
447     index.write()