]> arthur.barton.de Git - bup.git/blob - git.py
cmd-index.py: Retry os.open without O_LARGEFILE if not supported.
[bup.git] / git.py
1 import os, errno, zlib, time, sha, subprocess, struct, mmap, stat, re
2 from helpers import *
3
4 verbose = 0
5 home_repodir = os.path.expanduser('~/.bup')
6 repodir = None
7
8
9 class GitError(Exception):
10     pass
11
12
13 def repo(sub = ''):
14     global repodir
15     if not repodir:
16         raise GitError('You should call check_repo_or_die()')
17     gd = os.path.join(repodir, '.git')
18     if os.path.exists(gd):
19         repodir = gd
20     return os.path.join(repodir, sub)
21
22
23 class PackIndex:
24     def __init__(self, filename):
25         self.name = filename
26         f = open(filename)
27         self.map = mmap.mmap(f.fileno(), 0,
28                              mmap.MAP_SHARED, mmap.PROT_READ)
29         f.close()  # map will persist beyond file close
30         assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
31         self.fanout = list(struct.unpack('!256I',
32                                          str(buffer(self.map, 8, 256*4))))
33         self.fanout.append(0)  # entry "-1"
34         nsha = self.fanout[255]
35         self.ofstable = buffer(self.map,
36                                8 + 256*4 + nsha*20 + nsha*4,
37                                nsha*4)
38         self.ofs64table = buffer(self.map,
39                                  8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
40
41     def _ofs_from_idx(self, idx):
42         ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
43         if ofs & 0x80000000:
44             idx64 = ofs & 0x7fffffff
45             ofs = struct.unpack('!I',
46                                 str(buffer(self.ofs64table, idx64*8, 8)))[0]
47         return ofs
48
49     def _idx_from_hash(self, hash):
50         assert(len(hash) == 20)
51         b1 = ord(hash[0])
52         start = self.fanout[b1-1] # range -1..254
53         end = self.fanout[b1] # range 0..255
54         buf = buffer(self.map, 8 + 256*4, end*20)
55         want = buffer(hash)
56         while start < end:
57             mid = start + (end-start)/2
58             v = buffer(buf, mid*20, 20)
59             if v < want:
60                 start = mid+1
61             elif v > want:
62                 end = mid
63             else: # got it!
64                 return mid
65         return None
66         
67     def find_offset(self, hash):
68         idx = self._idx_from_hash(hash)
69         if idx != None:
70             return self._ofs_from_idx(idx)
71         return None
72
73     def exists(self, hash):
74         return (self._idx_from_hash(hash) != None) and True or None
75
76
77 class MultiPackIndex:
78     def __init__(self, dir):
79         self.dir = dir
80         self.also = {}
81         self.packs = []
82         for f in os.listdir(self.dir):
83             if f.endswith('.idx'):
84                 self.packs.append(PackIndex(os.path.join(self.dir, f)))
85
86     def exists(self, hash):
87         if hash in self.also:
88             return True
89         for i in range(len(self.packs)):
90             p = self.packs[i]
91             if p.exists(hash):
92                 # reorder so most recently used packs are searched first
93                 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
94                 return True
95         return None
96
97     def add(self, hash):
98         self.also[hash] = 1
99
100     def zap_also(self):
101         self.also = {}
102
103
104 def calc_hash(type, content):
105     header = '%s %d\0' % (type, len(content))
106     sum = sha.sha(header)
107     sum.update(content)
108     return sum.digest()
109
110
111 def _shalist_sort_key(ent):
112     (mode, name, id) = ent
113     if stat.S_ISDIR(int(mode, 8)):
114         return name + '/'
115     else:
116         return name
117
118
119 _typemap = dict(blob=3, tree=2, commit=1, tag=8)
120 class PackWriter:
121     def __init__(self, objcache_maker=None):
122         self.count = 0
123         self.outbytes = 0
124         self.filename = None
125         self.file = None
126         self.objcache_maker = objcache_maker
127         self.objcache = None
128
129     def __del__(self):
130         self.close()
131
132     def _make_objcache(self):
133         if not self.objcache:
134             if self.objcache_maker:
135                 self.objcache = self.objcache_maker()
136             else:
137                 self.objcache = MultiPackIndex(repo('objects/pack'))
138
139     def _open(self):
140         if not self.file:
141             self._make_objcache()
142             self.filename = repo('objects/bup%d' % os.getpid())
143             self.file = open(self.filename + '.pack', 'w+')
144             self.file.write('PACK\0\0\0\2\0\0\0\0')
145
146     def _raw_write(self, datalist):
147         self._open()
148         f = self.file
149         for d in datalist:
150             f.write(d)
151             self.outbytes += len(d)
152         self.count += 1
153
154     def _write(self, bin, type, content):
155         if verbose:
156             log('>')
157
158         out = []
159
160         sz = len(content)
161         szbits = (sz & 0x0f) | (_typemap[type]<<4)
162         sz >>= 4
163         while 1:
164             if sz: szbits |= 0x80
165             out.append(chr(szbits))
166             if not sz:
167                 break
168             szbits = sz & 0x7f
169             sz >>= 7
170
171         z = zlib.compressobj(1)
172         out.append(z.compress(content))
173         out.append(z.flush())
174
175         self._raw_write(out)
176         return bin
177
178     def breakpoint(self):
179         id = self._end()
180         self.outbytes = self.count = 0
181         return id
182
183     def write(self, type, content):
184         return self._write(calc_hash(type, content), type, content)
185
186     def exists(self, id):
187         if not self.objcache:
188             self._make_objcache()
189         return self.objcache.exists(id)
190
191     def maybe_write(self, type, content):
192         bin = calc_hash(type, content)
193         if not self.exists(bin):
194             self._write(bin, type, content)
195             self.objcache.add(bin)
196         return bin
197
198     def new_blob(self, blob):
199         return self.maybe_write('blob', blob)
200
201     def new_tree(self, shalist):
202         shalist = sorted(shalist, key = _shalist_sort_key)
203         l = ['%s %s\0%s' % (mode,name,bin) 
204              for (mode,name,bin) in shalist]
205         return self.maybe_write('tree', ''.join(l))
206
207     def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
208         l = []
209         if tree: l.append('tree %s' % tree.encode('hex'))
210         if parent: l.append('parent %s' % parent.encode('hex'))
211         if author: l.append('author %s %s' % (author, _git_date(adate)))
212         if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
213         l.append('')
214         l.append(msg)
215         return self.maybe_write('commit', '\n'.join(l))
216
217     def new_commit(self, parent, tree, msg):
218         now = time.time()
219         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
220         commit = self._new_commit(tree, parent,
221                                   userline, now, userline, now,
222                                   msg)
223         return commit
224
225     def abort(self):
226         f = self.file
227         if f:
228             self.file = None
229             f.close()
230             os.unlink(self.filename + '.pack')
231
232     def _end(self):
233         f = self.file
234         if not f: return None
235         self.file = None
236
237         # update object count
238         f.seek(8)
239         cp = struct.pack('!i', self.count)
240         assert(len(cp) == 4)
241         f.write(cp)
242
243         # calculate the pack sha1sum
244         f.seek(0)
245         sum = sha.sha()
246         while 1:
247             b = f.read(65536)
248             sum.update(b)
249             if not b: break
250         f.write(sum.digest())
251         
252         f.close()
253         self.objcache = None
254
255         p = subprocess.Popen(['git', 'index-pack', '-v',
256                               '--index-version=2',
257                               self.filename + '.pack'],
258                              preexec_fn = _gitenv,
259                              stdout = subprocess.PIPE)
260         out = p.stdout.read().strip()
261         _git_wait('git index-pack', p)
262         if not out:
263             raise GitError('git index-pack produced no output')
264         nameprefix = repo('objects/pack/%s' % out)
265         os.rename(self.filename + '.pack', nameprefix + '.pack')
266         os.rename(self.filename + '.idx', nameprefix + '.idx')
267         return nameprefix
268
269     def close(self):
270         return self._end()
271
272
273 def _git_date(date):
274     return time.strftime('%s %z', time.localtime(date))
275
276
277 def _gitenv():
278     os.environ['GIT_DIR'] = os.path.abspath(repo())
279
280
281 def read_ref(refname):
282     p = subprocess.Popen(['git', 'show-ref', '--', refname],
283                          preexec_fn = _gitenv,
284                          stdout = subprocess.PIPE)
285     out = p.stdout.read().strip()
286     rv = p.wait()  # not fatal
287     if rv:
288         assert(not out)
289     if out:
290         return out.split()[0].decode('hex')
291     else:
292         return None
293
294
295 def update_ref(refname, newval, oldval):
296     if not oldval:
297         oldval = ''
298     p = subprocess.Popen(['git', 'update-ref', '--', refname,
299                           newval.encode('hex'), oldval.encode('hex')],
300                          preexec_fn = _gitenv)
301     _git_wait('git update-ref', p)
302
303
304 def guess_repo(path=None):
305     global repodir
306     if path:
307         repodir = path
308     if not repodir:
309         repodir = os.environ.get('BUP_DIR')
310         if not repodir:
311             repodir = os.path.expanduser('~/.bup')
312
313
314 def init_repo(path=None):
315     guess_repo(path)
316     d = repo()
317     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
318         raise GitError('"%d" exists but is not a directory\n' % d)
319     p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
320                          preexec_fn = _gitenv)
321     _git_wait('git init', p)
322     p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
323                          stdout=sys.stderr, preexec_fn = _gitenv)
324     _git_wait('git config', p)
325
326
327 def check_repo_or_die(path=None):
328     guess_repo(path)
329     if not os.path.isdir(repo('objects/pack/.')):
330         if repodir == home_repodir:
331             init_repo()
332         else:
333             log('error: %r is not a bup/git repository\n' % repo())
334             exit(15)
335
336
337 def _treeparse(buf):
338     ofs = 0
339     while ofs < len(buf):
340         z = buf[ofs:].find('\0')
341         assert(z > 0)
342         spl = buf[ofs:ofs+z].split(' ', 1)
343         assert(len(spl) == 2)
344         sha = buf[ofs+z+1:ofs+z+1+20]
345         ofs += z+1+20
346         yield (spl[0], spl[1], sha)
347
348 _ver = None
349 def ver():
350     global _ver
351     if not _ver:
352         p = subprocess.Popen(['git', '--version'],
353                              stdout=subprocess.PIPE)
354         gvs = p.stdout.read()
355         _git_wait('git --version', p)
356         m = re.match(r'git version (\S+.\S+)', gvs)
357         if not m:
358             raise GitError('git --version weird output: %r' % gvs)
359         _ver = tuple(m.group(1).split('.'))
360     needed = ('1','5','4')
361     if _ver < needed:
362         raise GitError('git version %s or higher is required; you have %s'
363                        % ('.'.join(needed), '.'.join(_ver)))
364     return _ver
365
366
367 def _git_wait(cmd, p):
368     rv = p.wait()
369     if rv != 0:
370         raise GitError('%s returned %d' % (cmd, rv))
371
372
373 def _git_capture(argv):
374     p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
375     r = p.stdout.read()
376     _git_wait(repr(argv), p)
377     return r
378
379
380 _ver_warned = 0
381 class CatPipe:
382     def __init__(self):
383         global _ver_warned
384         wanted = ('1','5','6')
385         if ver() < wanted:
386             if not _ver_warned:
387                 log('warning: git version < %s; bup will be slow.\n'
388                     % '.'.join(wanted))
389                 _ver_warned = 1
390             self.get = self._slow_get
391         else:
392             self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
393                                       stdin=subprocess.PIPE, 
394                                       stdout=subprocess.PIPE,
395                                       preexec_fn = _gitenv)
396             self.get = self._fast_get
397
398     def _fast_get(self, id):
399         assert(id.find('\n') < 0)
400         assert(id.find('\r') < 0)
401         assert(id[0] != '-')
402         self.p.stdin.write('%s\n' % id)
403         hdr = self.p.stdout.readline()
404         if hdr.endswith(' missing\n'):
405             raise GitError('blob %r is missing' % id)
406         spl = hdr.split(' ')
407         if len(spl) != 3 or len(spl[0]) != 40:
408             raise GitError('expected blob, got %r' % spl)
409         (hex, type, size) = spl
410         yield type
411         for blob in chunkyreader(self.p.stdout, int(spl[2])):
412             yield blob
413         assert(self.p.stdout.readline() == '\n')
414
415     def _slow_get(self, id):
416         assert(id.find('\n') < 0)
417         assert(id.find('\r') < 0)
418         assert(id[0] != '-')
419         type = _git_capture(['git', 'cat-file', '-t', id]).strip()
420         yield type
421
422         p = subprocess.Popen(['git', 'cat-file', type, id],
423                              stdout=subprocess.PIPE,
424                              preexec_fn = _gitenv)
425         for blob in chunkyreader(p.stdout):
426             yield blob
427         _git_wait('git cat-file', p)
428
429     def _join(self, it):
430         type = it.next()
431         if type == 'blob':
432             for blob in it:
433                 yield blob
434         elif type == 'tree':
435             treefile = ''.join(it)
436             for (mode, name, sha) in _treeparse(treefile):
437                 for blob in self.join(sha.encode('hex')):
438                     yield blob
439         elif type == 'commit':
440             treeline = ''.join(it).split('\n')[0]
441             assert(treeline.startswith('tree '))
442             for blob in self.join(treeline[5:]):
443                 yield blob
444         else:
445             raise GitError('invalid object type %r: expected blob/tree/commit'
446                            % type)
447
448     def join(self, id):
449         for d in self._join(self.get(id)):
450             yield d
451         
452
453 def cat(id):
454     c = CatPipe()
455     for d in c.join(id):
456         yield d