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