Fix fastexport commit exporter, add test.
[jelmer/dulwich-libgit2.git] / dulwich / index.py
1 # index.py -- File parser/write for the git index file
2 # Copyright (C) 2008-2009 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 os
22 import stat
23 import struct
24
25 from dulwich.file import GitFile
26 from dulwich.objects import (
27     S_IFGITLINK,
28     S_ISGITLINK,
29     Tree,
30     hex_to_sha,
31     sha_to_hex,
32     )
33 from dulwich.pack import (
34     SHA1Reader,
35     SHA1Writer,
36     )
37
38
39 def pathsplit(path):
40     """Split a /-delimited path into a directory part and a basename.
41
42     :param path: The path to split.
43     :return: Tuple with directory name and basename
44     """
45     try:
46         (dirname, basename) = path.rsplit("/", 1)
47     except ValueError:
48         return ("", path)
49     else:
50         return (dirname, basename)
51
52
53 def pathjoin(*args):
54     """Join a /-delimited path.
55
56     """
57     return "/".join([p for p in args if p])
58
59
60 def read_cache_time(f):
61     """Read a cache time.
62     
63     :param f: File-like object to read from
64     :return: Tuple with seconds and nanoseconds
65     """
66     return struct.unpack(">LL", f.read(8))
67
68
69 def write_cache_time(f, t):
70     """Write a cache time.
71     
72     :param f: File-like object to write to
73     :param t: Time to write (as int, float or tuple with secs and nsecs)
74     """
75     if isinstance(t, int):
76         t = (t, 0)
77     elif isinstance(t, float):
78         (secs, nsecs) = divmod(t, 1.0)
79         t = (int(secs), int(nsecs * 1000000000))
80     elif not isinstance(t, tuple):
81         raise TypeError(t)
82     f.write(struct.pack(">LL", *t))
83
84
85 def read_cache_entry(f):
86     """Read an entry from a cache file.
87
88     :param f: File-like object to read from
89     :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
90     """
91     beginoffset = f.tell()
92     ctime = read_cache_time(f)
93     mtime = read_cache_time(f)
94     (dev, ino, mode, uid, gid, size, sha, flags, ) = \
95         struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
96     name = f.read((flags & 0x0fff))
97     # Padding:
98     real_size = ((f.tell() - beginoffset + 8) & ~7)
99     data = f.read((beginoffset + real_size) - f.tell())
100     return (name, ctime, mtime, dev, ino, mode, uid, gid, size, 
101             sha_to_hex(sha), flags & ~0x0fff)
102
103
104 def write_cache_entry(f, entry):
105     """Write an index entry to a file.
106
107     :param f: File object
108     :param entry: Entry to write, tuple with: 
109         (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
110     """
111     beginoffset = f.tell()
112     (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
113     write_cache_time(f, ctime)
114     write_cache_time(f, mtime)
115     flags = len(name) | (flags &~ 0x0fff)
116     f.write(struct.pack(">LLLLLL20sH", dev, ino, mode, uid, gid, size, hex_to_sha(sha), flags))
117     f.write(name)
118     real_size = ((f.tell() - beginoffset + 8) & ~7)
119     f.write("\0" * ((beginoffset + real_size) - f.tell()))
120
121
122 def read_index(f):
123     """Read an index file, yielding the individual entries."""
124     header = f.read(4)
125     if header != "DIRC":
126         raise AssertionError("Invalid index file header: %r" % header)
127     (version, num_entries) = struct.unpack(">LL", f.read(4 * 2))
128     assert version in (1, 2)
129     for i in range(num_entries):
130         yield read_cache_entry(f)
131
132
133 def read_index_dict(f):
134     """Read an index file and return it as a dictionary.
135     
136     :param f: File object to read from
137     """
138     ret = {}
139     for x in read_index(f):
140         ret[x[0]] = tuple(x[1:])
141     return ret
142
143
144 def write_index(f, entries):
145     """Write an index file.
146     
147     :param f: File-like object to write to
148     :param entries: Iterable over the entries to write
149     """
150     f.write("DIRC")
151     f.write(struct.pack(">LL", 2, len(entries)))
152     for x in entries:
153         write_cache_entry(f, x)
154
155
156 def write_index_dict(f, entries):
157     """Write an index file based on the contents of a dictionary.
158
159     """
160     entries_list = []
161     for name in sorted(entries):
162         entries_list.append((name,) + tuple(entries[name]))
163     write_index(f, entries_list)
164
165
166 def cleanup_mode(mode):
167     """Cleanup a mode value.
168
169     This will return a mode that can be stored in a tree object.
170     
171     :param mode: Mode to clean up.
172     """
173     if stat.S_ISLNK(mode):
174         return stat.S_IFLNK
175     elif stat.S_ISDIR(mode):
176         return stat.S_IFDIR
177     elif S_ISGITLINK(mode):
178         return S_IFGITLINK
179     ret = stat.S_IFREG | 0644
180     ret |= (mode & 0111)
181     return ret
182
183
184 class Index(object):
185     """A Git Index file."""
186
187     def __init__(self, filename):
188         """Open an index file.
189         
190         :param filename: Path to the index file
191         """
192         self._filename = filename
193         self.clear()
194         self.read()
195
196     def write(self):
197         """Write current contents of index to disk."""
198         f = GitFile(self._filename, 'wb')
199         try:
200             f = SHA1Writer(f)
201             write_index_dict(f, self._byname)
202         finally:
203             f.close()
204
205     def read(self):
206         """Read current contents of index from disk."""
207         f = GitFile(self._filename, 'rb')
208         try:
209             f = SHA1Reader(f)
210             for x in read_index(f):
211                 self[x[0]] = tuple(x[1:])
212             # FIXME: Additional data?
213             f.read(os.path.getsize(self._filename)-f.tell()-20)
214             f.check_sha()
215         finally:
216             f.close()
217
218     def __len__(self):
219         """Number of entries in this index file."""
220         return len(self._byname)
221
222     def __getitem__(self, name):
223         """Retrieve entry by relative path.
224         
225         :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
226         """
227         return self._byname[name]
228
229     def __iter__(self):
230         """Iterate over the paths in this index."""
231         return iter(self._byname)
232
233     def get_sha1(self, path):
234         """Return the (git object) SHA1 for the object at a path."""
235         return self[path][-2]
236
237     def get_mode(self, path):
238         """Return the POSIX file mode for the object at a path."""
239         return self[path][-6]
240
241     def iterblobs(self):
242         """Iterate over path, sha, mode tuples for use with commit_tree."""
243         for path in self:
244             entry = self[path]
245             yield path, entry[-2], cleanup_mode(entry[-6])
246
247     def clear(self):
248         """Remove all contents from this index."""
249         self._byname = {}
250
251     def __setitem__(self, name, x):
252         assert isinstance(name, str)
253         assert len(x) == 10
254         # Remove the old entry if any
255         self._byname[name] = x
256
257     def iteritems(self):
258         return self._byname.iteritems()
259
260     def update(self, entries):
261         for name, value in entries.iteritems():
262             self[name] = value
263
264     def changes_from_tree(self, object_store, tree, want_unchanged=False):
265         """Find the differences between the contents of this index and a tree.
266
267         :param object_store: Object store to use for retrieving tree contents
268         :param tree: SHA1 of the root tree
269         :param want_unchanged: Whether unchanged files should be reported
270         :return: Iterator over tuples with (oldpath, newpath), (oldmode, newmode), (oldsha, newsha)
271         """
272         mine = set(self._byname.keys())
273         for (name, mode, sha) in object_store.iter_tree_contents(tree):
274             if name in mine:
275                 if (want_unchanged or self.get_sha1(name) != sha or 
276                     self.get_mode(name) != mode):
277                     yield ((name, name), (mode, self.get_mode(name)), (sha, self.get_sha1(name)))
278                 mine.remove(name)
279             else:
280                 # Was removed
281                 yield ((name, None), (mode, None), (sha, None))
282         # Mention added files
283         for name in mine:
284             yield ((None, name), (None, self.get_mode(name)), (None, self.get_sha1(name)))
285
286     def commit(self, object_store):
287         """Create a new tree from an index.
288
289         :param object_store: Object store to save the tree in
290         :return: Root tree SHA
291         """
292         return commit_tree(object_store, self.iterblobs())
293
294
295 def commit_tree(object_store, blobs):
296     """Commit a new tree.
297
298     :param object_store: Object store to add trees to
299     :param blobs: Iterable over blob path, sha, mode entries
300     :return: SHA1 of the created tree.
301     """
302     trees = {"": {}}
303     def add_tree(path):
304         if path in trees:
305             return trees[path]
306         dirname, basename = pathsplit(path)
307         t = add_tree(dirname)
308         assert isinstance(basename, str)
309         newtree = {}
310         t[basename] = newtree
311         trees[path] = newtree
312         return newtree
313
314     for path, sha, mode in blobs:
315         tree_path, basename = pathsplit(path)
316         tree = add_tree(tree_path)
317         tree[basename] = (mode, sha)
318
319     def build_tree(path):
320         tree = Tree()
321         for basename, entry in trees[path].iteritems():
322             if type(entry) == dict:
323                 mode = stat.S_IFDIR
324                 sha = build_tree(pathjoin(path, basename))
325             else:
326                 (mode, sha) = entry
327             tree.add(mode, basename, sha)
328         object_store.add_object(tree)
329         return tree.id
330     return build_tree("")
331
332
333 def commit_index(object_store, index):
334     """Create a new tree from an index.
335
336     :param object_store: Object store to save the tree in
337     :param index: Index file
338     :note: This function is deprecated, use index.commit() instead.
339     :return: Root tree sha.
340     """
341     return commit_tree(object_store, index.iterblobs())