aa57e156034d64dd95ffd78752bdbd48af058f3b
[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.objects import (
26     S_IFGITLINK,
27     S_ISGITLINK,
28     Tree,
29     hex_to_sha,
30     sha_to_hex,
31     )
32 from dulwich.pack import (
33     SHA1Reader,
34     SHA1Writer,
35     )
36
37
38 def read_cache_time(f):
39     """Read a cache time.
40     
41     :param f: File-like object to read from
42     :return: Tuple with seconds and nanoseconds
43     """
44     return struct.unpack(">LL", f.read(8))
45
46
47 def write_cache_time(f, t):
48     """Write a cache time.
49     
50     :param f: File-like object to write to
51     :param t: Time to write (as int, float or tuple with secs and nsecs)
52     """
53     if isinstance(t, int):
54         t = (t, 0)
55     elif isinstance(t, float):
56         (secs, nsecs) = divmod(t, 1.0)
57         t = (int(secs), int(nsecs * 1000000000))
58     elif not isinstance(t, tuple):
59         raise TypeError(t)
60     f.write(struct.pack(">LL", *t))
61
62
63 def read_cache_entry(f):
64     """Read an entry from a cache file.
65
66     :param f: File-like object to read from
67     :return: tuple with: device, inode, mode, uid, gid, size, sha, flags
68     """
69     beginoffset = f.tell()
70     ctime = read_cache_time(f)
71     mtime = read_cache_time(f)
72     (dev, ino, mode, uid, gid, size, sha, flags, ) = \
73         struct.unpack(">LLLLLL20sH", f.read(20 + 4 * 6 + 2))
74     name = f.read((flags & 0x0fff))
75     # Padding:
76     real_size = ((f.tell() - beginoffset + 8) & ~7)
77     data = f.read((beginoffset + real_size) - f.tell())
78     return (name, ctime, mtime, dev, ino, mode, uid, gid, size, 
79             sha_to_hex(sha), flags & ~0x0fff)
80
81
82 def write_cache_entry(f, entry):
83     """Write an index entry to a file.
84
85     :param f: File object
86     :param entry: Entry to write, tuple with: 
87         (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
88     """
89     beginoffset = f.tell()
90     (name, ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = entry
91     write_cache_time(f, ctime)
92     write_cache_time(f, mtime)
93     flags = len(name) | (flags &~ 0x0fff)
94     f.write(struct.pack(">LLLLLL20sH", dev, ino, mode, uid, gid, size, hex_to_sha(sha), flags))
95     f.write(name)
96     real_size = ((f.tell() - beginoffset + 8) & ~7)
97     f.write("\0" * ((beginoffset + real_size) - f.tell()))
98
99
100 def read_index(f):
101     """Read an index file, yielding the individual entries."""
102     header = f.read(4)
103     if header != "DIRC":
104         raise AssertionError("Invalid index file header: %r" % header)
105     (version, num_entries) = struct.unpack(">LL", f.read(4 * 2))
106     assert version in (1, 2)
107     for i in range(num_entries):
108         yield read_cache_entry(f)
109
110
111 def read_index_dict(f):
112     """Read an index file and return it as a dictionary.
113     
114     :param f: File object to read from
115     """
116     ret = {}
117     for x in read_index(f):
118         ret[x[0]] = tuple(x[1:])
119     return ret
120
121
122 def write_index(f, entries):
123     """Write an index file.
124     
125     :param f: File-like object to write to
126     :param entries: Iterable over the entries to write
127     """
128     f.write("DIRC")
129     f.write(struct.pack(">LL", 2, len(entries)))
130     for x in entries:
131         write_cache_entry(f, x)
132
133
134 def write_index_dict(f, entries):
135     """Write an index file based on the contents of a dictionary.
136
137     """
138     entries_list = []
139     for name in sorted(entries):
140         entries_list.append((name,) + tuple(entries[name]))
141     write_index(f, entries_list)
142
143
144 def cleanup_mode(mode):
145     """Cleanup a mode value.
146
147     This will return a mode that can be stored in a tree object.
148     
149     :param mode: Mode to clean up.
150     """
151     if stat.S_ISLNK(mode):
152         return stat.S_IFLNK
153     elif stat.S_ISDIR(mode):
154         return stat.S_IFDIR
155     elif S_ISGITLINK(mode):
156         return S_IFGITLINK
157     ret = stat.S_IFREG | 0644
158     ret |= (mode & 0111)
159     return ret
160
161
162 class Index(object):
163     """A Git Index file."""
164
165     def __init__(self, filename):
166         """Open an index file.
167         
168         :param filename: Path to the index file
169         """
170         self._filename = filename
171         self.clear()
172         self.read()
173
174     def write(self):
175         """Write current contents of index to disk."""
176         f = open(self._filename, 'wb')
177         try:
178             f = SHA1Writer(f)
179             write_index_dict(f, self._byname)
180         finally:
181             f.close()
182
183     def read(self):
184         """Read current contents of index from disk."""
185         f = open(self._filename, 'rb')
186         try:
187             f = SHA1Reader(f)
188             for x in read_index(f):
189                 self[x[0]] = tuple(x[1:])
190             # FIXME: Additional data?
191             f.read(os.path.getsize(self._filename)-f.tell()-20)
192             f.check_sha()
193         finally:
194             f.close()
195
196     def __len__(self):
197         """Number of entries in this index file."""
198         return len(self._byname)
199
200     def __getitem__(self, name):
201         """Retrieve entry by relative path.
202         
203         :return: tuple with (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags)
204         """
205         return self._byname[name]
206
207     def __iter__(self):
208         """Iterate over the paths in this index."""
209         return iter(self._byname)
210
211     def get_sha1(self, path):
212         """Return the (git object) SHA1 for the object at a path."""
213         return self[path][-2]
214
215     def iterblobs(self):
216         """Iterate over path, sha, mode tuples for use with commit_tree."""
217         for path, entry in self:
218             yield path, entry[-2], cleanup_mode(entry[-6])
219
220     def clear(self):
221         """Remove all contents from this index."""
222         self._byname = {}
223
224     def __setitem__(self, name, x):
225         assert isinstance(name, str)
226         assert len(x) == 10
227         # Remove the old entry if any
228         self._byname[name] = x
229
230     def iteritems(self):
231         return self._byname.iteritems()
232
233     def update(self, entries):
234         for name, value in entries.iteritems():
235             self[name] = value
236
237
238 def commit_tree(object_store, blobs):
239     """Commit a new tree.
240
241     :param object_store: Object store to add trees to
242     :param blobs: Iterable over blob path, sha, mode entries
243     :return: SHA1 of the created tree.
244     """
245     trees = {"": {}}
246     def add_tree(path):
247         if path in trees:
248             return trees[path]
249         dirname, basename = os.path.split(path)
250         t = add_tree(dirname)
251         assert isinstance(basename, str)
252         newtree = {}
253         t[basename] = newtree
254         trees[path] = newtree
255         return newtree
256
257     for path, sha, mode in blobs:
258         tree_path, basename = os.path.split(path)
259         tree = add_tree(tree_path)
260         tree[basename] = (mode, sha)
261
262     def build_tree(path):
263         tree = Tree()
264         for basename, entry in trees[path].iteritems():
265             if type(entry) == dict:
266                 mode = stat.S_IFDIR
267                 sha = build_tree(os.path.join(path, basename))
268             else:
269                 (mode, sha) = entry
270             tree.add(mode, basename, sha)
271         object_store.add_object(tree)
272         return tree.id
273     return build_tree("")
274
275
276 def commit_index(object_store, index):
277     """Create a new tree from an index.
278
279     :param object_store: Object store to save the tree in
280     :param index: Index file
281     """
282     return commit_tree(object_store, index.iterblobs())