]> arthur.barton.de Git - bup.git/blob - index.py
Move testfile[12] into t/
[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\1'
7 INDEX_SIG = '!IIIIIQII20sH'
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 class Entry:
18     def __init__(self, name, m, ofs, tstart):
19         self._m = m
20         self._ofs = ofs
21         self.name = str(name)
22         self.tstart = tstart
23         (self.dev, self.ctime, self.mtime, self.uid, self.gid,
24          self.size, self.mode, self.gitmode, self.sha,
25          self.flags) = struct.unpack(INDEX_SIG, str(buffer(m, ofs, ENTLEN)))
26
27     def __repr__(self):
28         return ("(%s,0x%04x,%d,%d,%d,%d,%d,0x%04x)" 
29                 % (self.name, self.dev,
30                    self.ctime, self.mtime, self.uid, self.gid,
31                    self.size, self.flags))
32
33     def packed(self):
34         return struct.pack(INDEX_SIG,
35                            self.dev, self.ctime, self.mtime, 
36                            self.uid, self.gid, self.size, self.mode,
37                            self.gitmode, self.sha, self.flags)
38
39     def repack(self):
40         self._m[self._ofs:self._ofs+ENTLEN] = self.packed()
41
42     def from_stat(self, st):
43         old = (self.dev, self.ctime, self.mtime,
44                self.uid, self.gid, self.size, self.flags & IX_EXISTS)
45         new = (st.st_dev, int(st.st_ctime), int(st.st_mtime),
46                st.st_uid, st.st_gid, st.st_size, IX_EXISTS)
47         self.dev = st.st_dev
48         self.ctime = int(st.st_ctime)
49         self.mtime = int(st.st_mtime)
50         self.uid = st.st_uid
51         self.gid = st.st_gid
52         self.size = st.st_size
53         self.mode = st.st_mode
54         self.flags |= IX_EXISTS
55         if int(st.st_ctime) >= self.tstart or old != new:
56             self.flags &= ~IX_HASHVALID
57             return 1  # dirty
58         else:
59             return 0  # not dirty
60
61     def validate(self, sha):
62         assert(sha)
63         self.sha = sha
64         self.flags |= IX_HASHVALID
65
66     def __cmp__(a, b):
67         return cmp(a.name, b.name)
68             
69
70 class Reader:
71     def __init__(self, filename):
72         self.filename = filename
73         self.m = ''
74         self.writable = False
75         f = None
76         try:
77             f = open(filename, 'r+')
78         except IOError, e:
79             if e.errno == errno.ENOENT:
80                 pass
81             else:
82                 raise
83         if f:
84             b = f.read(len(INDEX_HDR))
85             if b != INDEX_HDR:
86                 raise Error('%s: header: expected %r, got %r'
87                                  % (filename, INDEX_HDR, b))
88             st = os.fstat(f.fileno())
89             if st.st_size:
90                 self.m = mmap_readwrite(f)
91                 self.writable = True
92
93     def __del__(self):
94         self.close()
95
96     def __iter__(self):
97         tstart = int(time.time())
98         ofs = len(INDEX_HDR)
99         while ofs < len(self.m):
100             eon = self.m.find('\0', ofs)
101             assert(eon >= 0)
102             yield Entry(buffer(self.m, ofs, eon-ofs),
103                           self.m, eon+1, tstart = tstart)
104             ofs = eon + 1 + ENTLEN
105
106     def save(self):
107         if self.writable and self.m:
108             self.m.flush()
109
110     def close(self):
111         self.save()
112         if self.writable and self.m:
113             self.m = None
114             self.writable = False
115
116     def filter(self, prefixes):
117         #log("filtering %r\n" % prefixes)
118         paths = reduce_paths(prefixes)
119         #log("filtering %r\n" % paths)
120         pi = iter(paths)
121         (rpin, pin) = pi.next()
122         for ent in self:
123             #log('checking %r vs %r\n' % (ent.name, rpin))
124             while ent.name < rpin:
125                 try:
126                     (rpin, pin) = pi.next()
127                 except StopIteration:
128                     return  # no more files can possibly match
129             if not ent.name.startswith(rpin):
130                 continue   # not interested
131             else:
132                 name = pin + ent.name[len(rpin):]
133                 yield (name, ent)
134
135
136 # Read all the iters in order; when more than one iter has the same entry,
137 # the *later* iter in the list wins.  (ie. more recent iter entries replace
138 # older ones)
139 def _last_writer_wins_iter(iters):
140     l = []
141     for e in iters:
142         it = iter(e)
143         try:
144             l.append([it.next(), it])
145         except StopIteration:
146             pass
147     del iters  # to avoid accidents
148     while l:
149         l.sort()
150         mv = l[0][0]
151         mi = []
152         for (i,(v,it)) in enumerate(l):
153             #log('(%d) considering %d: %r\n' % (len(l), i, v))
154             if v > mv:
155                 mv = v
156                 mi = [i]
157             elif v == mv:
158                 mi.append(i)
159         yield mv
160         for i in mi:
161             try:
162                 l[i][0] = l[i][1].next()
163             except StopIteration:
164                 l[i] = None
165         l = filter(None, l)
166
167
168 class Writer:
169     def __init__(self, filename):
170         self.f = None
171         self.count = 0
172         self.lastfile = None
173         self.filename = None
174         self.filename = filename = realpath(filename)
175         (dir,name) = os.path.split(filename)
176         (ffd,self.tmpname) = tempfile.mkstemp('.tmp', filename, dir)
177         self.f = os.fdopen(ffd, 'wb', 65536)
178         self.f.write(INDEX_HDR)
179
180     def __del__(self):
181         self.abort()
182
183     def abort(self):
184         f = self.f
185         self.f = None
186         if f:
187             f.close()
188             os.unlink(self.tmpname)
189
190     def close(self):
191         f = self.f
192         self.f = None
193         if f:
194             f.close()
195             os.rename(self.tmpname, self.filename)
196
197     def _write(self, data):
198         self.f.write(data)
199         self.count += 1
200
201     def add(self, name, st, hashgen=None):
202         #log('ADDING %r\n' % name)
203         if self.lastfile:
204             assert(cmp(self.lastfile, name) > 0) # reverse order only
205         self.lastfile = name
206         flags = IX_EXISTS
207         sha = None
208         if hashgen:
209             (gitmode, sha) = hashgen(name)
210             if sha:
211                 flags |= IX_HASHVALID
212         else:
213             (gitmode, sha) = (0, EMPTY_SHA)
214         data = name + '\0' + \
215             struct.pack(INDEX_SIG, st.st_dev, int(st.st_ctime),
216                         int(st.st_mtime), st.st_uid, st.st_gid,
217                         st.st_size, st.st_mode, gitmode, sha, flags)
218         self._write(data)
219
220     def add_ixentry(self, e):
221         if self.lastfile and self.lastfile <= e.name:
222             raise Error('%r must come before %r' 
223                              % (e.name, self.lastfile))
224         self.lastfile = e.name
225         data = e.name + '\0' + e.packed()
226         self._write(data)
227
228     def new_reader(self):
229         self.f.flush()
230         return Reader(self.tmpname)
231
232
233 # like os.path.realpath, but doesn't follow a symlink for the last element.
234 # (ie. if 'p' itself is itself a symlink, this one won't follow it)
235 def realpath(p):
236     try:
237         st = os.lstat(p)
238     except OSError:
239         st = None
240     if st and stat.S_ISLNK(st.st_mode):
241         (dir, name) = os.path.split(p)
242         dir = os.path.realpath(dir)
243         out = os.path.join(dir, name)
244     else:
245         out = os.path.realpath(p)
246     #log('realpathing:%r,%r\n' % (p, out))
247     return out
248
249
250 def reduce_paths(paths):
251     xpaths = []
252     for p in paths:
253         rp = realpath(p)
254         st = os.lstat(rp)
255         if stat.S_ISDIR(st.st_mode):
256             rp = slashappend(rp)
257             p = slashappend(p)
258         xpaths.append((rp, p))
259     xpaths.sort()
260
261     paths = []
262     prev = None
263     for (rp, p) in xpaths:
264         if prev and (prev == rp 
265                      or (prev.endswith('/') and rp.startswith(prev))):
266             continue # already superceded by previous path
267         paths.append((rp, p))
268         prev = rp
269     paths.sort(reverse=True)
270     return paths
271