]> arthur.barton.de Git - bup.git/blob - index.py
Speed up cmd-drecurse by 40%.
[bup.git] / index.py
1 import os, stat, time, struct, tempfile
2 from helpers import *
3
4 EMPTY_SHA = '\0'*20
5 FAKE_SHA = '\x01'*20
6 INDEX_HDR = 'BUPI\0\0\0\2'
7 INDEX_SIG = '!IIIIIQII20sHII'
8 ENTLEN = struct.calcsize(INDEX_SIG)
9
10 IX_EXISTS = 0x8000
11 IX_HASHVALID = 0x4000
12
13 class Error(Exception):
14     pass
15
16
17 def _encode(dev, ctime, mtime, uid, gi8d, size, mode, gitmode, sha, flags):
18     return struct.pack(INDEX_SIG,
19                        dev, ctime, mtime, uid, gid, size, mode,
20                        gitmode, sha, flags)
21
22 class Entry:
23     def __init__(self, name):
24         self.name = str(name)
25         self.children_ofs = 0
26         self.children_n = 0
27
28     def __repr__(self):
29         return ("(%s,0x%04x,%d,%d,%d,%d,%d,0x%04x)" 
30                 % (self.name, self.dev,
31                    self.ctime, self.mtime, self.uid, self.gid,
32                    self.size, self.flags))
33
34     def packed(self):
35         return struct.pack(INDEX_SIG,
36                            self.dev, self.ctime, self.mtime, 
37                            self.uid, self.gid, self.size, self.mode,
38                            self.gitmode, self.sha, self.flags,
39                            self.children_ofs, self.children_n)
40
41     def from_stat(self, st, tstart):
42         old = (self.dev, self.ctime, self.mtime,
43                self.uid, self.gid, self.size, self.flags & IX_EXISTS)
44         new = (st.st_dev, int(st.st_ctime), int(st.st_mtime),
45                st.st_uid, st.st_gid, st.st_size, IX_EXISTS)
46         self.dev = st.st_dev
47         self.ctime = int(st.st_ctime)
48         self.mtime = int(st.st_mtime)
49         self.uid = st.st_uid
50         self.gid = st.st_gid
51         self.size = st.st_size
52         self.mode = st.st_mode
53         self.flags |= IX_EXISTS
54         if int(st.st_ctime) >= tstart or old != new:
55             self.flags &= ~IX_HASHVALID
56             self.set_dirty()
57
58     def validate(self, sha):
59         assert(sha)
60         self.sha = sha
61         self.flags |= IX_HASHVALID
62
63     def set_deleted(self):
64         self.flags &= ~(IX_EXISTS | IX_HASHVALID)
65         self.set_dirty()
66
67     def set_dirty(self):
68         pass # FIXME
69
70     def __cmp__(a, b):
71         return cmp(a.name, b.name)
72
73
74 class NewEntry(Entry):
75     def __init__(self, name, dev, ctime, mtime, uid, gid,
76                  size, mode, gitmode, sha, flags, children_ofs, children_n):
77         Entry.__init__(self, name)
78         (self.dev, self.ctime, self.mtime, self.uid, self.gid,
79          self.size, self.mode, self.gitmode, self.sha,
80          self.flags, self.children_ofs, self.children_n
81          ) = (dev, int(ctime), int(mtime), uid, gid,
82               size, mode, gitmode, sha, flags, children_ofs, children_n)
83
84
85 class ExistingEntry(Entry):
86     def __init__(self, name, m, ofs):
87         Entry.__init__(self, name)
88         self._m = m
89         self._ofs = ofs
90         (self.dev, self.ctime, self.mtime, self.uid, self.gid,
91          self.size, self.mode, self.gitmode, self.sha,
92          self.flags, self.children_ofs, self.children_n
93          ) = struct.unpack(INDEX_SIG, str(buffer(m, ofs, ENTLEN)))
94
95     def repack(self):
96         self._m[self._ofs:self._ofs+ENTLEN] = self.packed()
97
98     def iter(self, name=None):
99         dname = name
100         if dname and not dname.endswith('/'):
101             dname += '/'
102         ofs = self.children_ofs
103         #log('myname=%r\n' % self.name)
104         assert(ofs <= len(self._m))
105         for i in range(self.children_n):
106             eon = self._m.find('\0', ofs)
107             #log('eon=0x%x ofs=0x%x i=%d cn=%d\n' % (eon, ofs, i, self.children_n))
108             assert(eon >= 0)
109             assert(eon >= ofs)
110             assert(eon > ofs)
111             child = ExistingEntry(self.name + str(buffer(self._m, ofs, eon-ofs)),
112                                   self._m, eon+1)
113             if (not dname
114                  or child.name.startswith(dname)
115                  or child.name.endswith('/') and dname.startswith(child.name)):
116                 for e in child.iter(name=name):
117                     yield e
118             if not name or child.name == name or child.name.startswith(dname):
119                 yield child
120             ofs = eon + 1 + ENTLEN
121
122     def __iter__(self):
123         return self.iter()
124             
125
126 class Reader:
127     def __init__(self, filename):
128         self.filename = filename
129         self.m = ''
130         self.writable = False
131         f = None
132         try:
133             f = open(filename, 'r+')
134         except IOError, e:
135             if e.errno == errno.ENOENT:
136                 pass
137             else:
138                 raise
139         if f:
140             b = f.read(len(INDEX_HDR))
141             if b != INDEX_HDR:
142                 log('warning: %s: header: expected %r, got %r'
143                                  % (filename, INDEX_HDR, b))
144             else:
145                 st = os.fstat(f.fileno())
146                 if st.st_size:
147                     self.m = mmap_readwrite(f)
148                     self.writable = True
149
150     def __del__(self):
151         self.close()
152
153     def iter(self, name=None):
154         if len(self.m) > len(INDEX_HDR)+ENTLEN:
155             dname = name
156             if dname and not dname.endswith('/'):
157                 dname += '/'
158             root = ExistingEntry('/', self.m, len(self.m)-ENTLEN)
159             for sub in root.iter(name=name):
160                 yield sub
161             if not dname or dname == root.name:
162                 yield root
163
164     def __iter__(self):
165         return self.iter()
166
167     def exists(self):
168         return self.m
169
170     def save(self):
171         if self.writable and self.m:
172             self.m.flush()
173
174     def close(self):
175         self.save()
176         if self.writable and self.m:
177             self.m = None
178             self.writable = False
179
180     def filter(self, prefixes):
181         for (rp, path) in reduce_paths(prefixes):
182             for e in self.iter(rp):
183                 assert(e.name.startswith(rp))
184                 name = path + e.name[len(rp):]
185                 yield (name, e)
186
187
188 # Read all the iters in order; when more than one iter has the same entry,
189 # the *later* iter in the list wins.  (ie. more recent iter entries replace
190 # older ones)
191 def _last_writer_wins_iter(iters):
192     l = []
193     for e in iters:
194         it = iter(e)
195         try:
196             l.append([it.next(), it])
197         except StopIteration:
198             pass
199     del iters  # to avoid accidents
200     while l:
201         l.sort()
202         mv = l[0][0]
203         mi = []
204         for (i,(v,it)) in enumerate(l):
205             #log('(%d) considering %d: %r\n' % (len(l), i, v))
206             if v > mv:
207                 mv = v
208                 mi = [i]
209             elif v == mv:
210                 mi.append(i)
211         yield mv
212         for i in mi:
213             try:
214                 l[i][0] = l[i][1].next()
215             except StopIteration:
216                 l[i] = None
217         l = filter(None, l)
218
219
220 class Writer:
221     def __init__(self, filename):
222         self.stack = []
223         self.f = None
224         self.count = 0
225         self.lastfile = None
226         self.filename = None
227         self.filename = filename = realpath(filename)
228         (dir,name) = os.path.split(filename)
229         (ffd,self.tmpname) = tempfile.mkstemp('.tmp', filename, dir)
230         self.f = os.fdopen(ffd, 'wb', 65536)
231         self.f.write(INDEX_HDR)
232
233     def __del__(self):
234         self.abort()
235
236     def abort(self):
237         f = self.f
238         self.f = None
239         if f:
240             f.close()
241             os.unlink(self.tmpname)
242
243     def flush(self):
244         while self.stack:
245             self.add(''.join(self.stack[-1][0]), None)
246         self._pop_to(None, [])
247         self.f.flush()
248
249     def close(self):
250         self.flush()
251         f = self.f
252         self.f = None
253         if f:
254             f.close()
255             os.rename(self.tmpname, self.filename)
256
257     # FIXME: this function modifies 'entry' and can only pop a single level.
258     # That means its semantics are basically crazy.
259     def _pop_to(self, entry, edir):
260         assert(len(self.stack) - len(edir) <= 1)
261         while self.stack and self.stack[-1][0] > edir:
262             #log('popping %r with %d entries (%d)\n' 
263             #    % (''.join(self.stack[-1][0]), len(self.stack[-1][1]),
264             #       len(self.stack)))
265             p = self.stack.pop()
266             entry.children_ofs = self.f.tell()
267             entry.children_n = len(p[1])
268             for e in p[1]:
269                 self._write(e)
270
271     def _write(self, entry):
272         #log('        writing %r\n' % entry.name)
273         es = pathsplit(entry.name)
274         self.f.write(es[-1] + '\0' + entry.packed())
275         self.count += 1
276
277     def _add(self, entry):
278         es = pathsplit(entry.name)
279         edir = es[:-1]
280         self._pop_to(entry, edir)
281         while len(self.stack) < len(edir):
282             self.stack.append([es[:len(self.stack)+1], [], ()])
283         if entry.name != '/':
284             self.stack[-1][1].append(entry)
285         else:
286             self._write(entry)
287
288     def add(self, name, st, hashgen = None):
289         if self.lastfile:
290             assert(cmp(self.lastfile, name) > 0) # reverse order only
291         endswith = name.endswith('/')
292         flags = IX_EXISTS
293         sha = None
294         if hashgen:
295             (gitmode, sha) = hashgen(name)
296             flags |= IX_HASHVALID
297         else:
298             (gitmode, sha) = (0, EMPTY_SHA)
299         if st:
300             isdir = stat.S_ISDIR(st.st_mode)
301             assert(isdir == endswith)
302             e = NewEntry(name, st.st_dev, int(st.st_ctime),
303                          int(st.st_mtime), st.st_uid, st.st_gid,
304                          st.st_size, st.st_mode, gitmode, sha, flags,
305                          0, 0)
306         else:
307             assert(endswith)
308             e = NewEntry(name, 0, 0, 0, 0, 0, 0, 0, gitmode, sha, flags, 0, 0)
309         self.lastfile = name
310         self._add(e)
311
312     def add_ixentry(self, e):
313         if self.lastfile and self.lastfile <= e.name:
314             raise Error('%r must come before %r' 
315                              % (e.name, self.lastfile))
316         self.lastfile = e.name
317         self._add(e)
318
319     def new_reader(self):
320         self.flush()
321         return Reader(self.tmpname)
322
323
324 def reduce_paths(paths):
325     xpaths = []
326     for p in paths:
327         rp = realpath(p)
328         try:
329             st = os.lstat(rp)
330             if stat.S_ISDIR(st.st_mode):
331                 rp = slashappend(rp)
332                 p = slashappend(p)
333         except OSError, e:
334             if e.errno != errno.ENOENT:
335                 raise
336         xpaths.append((rp, p))
337     xpaths.sort()
338
339     paths = []
340     prev = None
341     for (rp, p) in xpaths:
342         if prev and (prev == rp 
343                      or (prev.endswith('/') and rp.startswith(prev))):
344             continue # already superceded by previous path
345         paths.append((rp, p))
346         prev = rp
347     paths.sort(reverse=True)
348     return paths
349