]> arthur.barton.de Git - bup.git/blob - lib/bup/git.py
Stop using '%s' formatter in strftime.
[bup.git] / lib / bup / git.py
1 import os, errno, zlib, time, subprocess, struct, stat, re, tempfile
2 import heapq
3 from bup.helpers import *
4
5 verbose = 0
6 ignore_midx = 0
7 home_repodir = os.path.expanduser('~/.bup')
8 repodir = None
9
10 _typemap =  { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
11 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
12
13
14 class GitError(Exception):
15     pass
16
17
18 def repo(sub = ''):
19     global repodir
20     if not repodir:
21         raise GitError('You should call check_repo_or_die()')
22     gd = os.path.join(repodir, '.git')
23     if os.path.exists(gd):
24         repodir = gd
25     return os.path.join(repodir, sub)
26
27
28 def mangle_name(name, mode, gitmode):
29     if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
30         return name + '.bup'
31     elif name.endswith('.bup') or name[:-1].endswith('.bup'):
32         return name + '.bupl'
33     else:
34         return name
35
36
37 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
38 def demangle_name(name):
39     if name.endswith('.bupl'):
40         return (name[:-5], BUP_NORMAL)
41     elif name.endswith('.bup'):
42         return (name[:-4], BUP_CHUNKED)
43     else:
44         return (name, BUP_NORMAL)
45
46
47 def _encode_packobj(type, content):
48     szout = ''
49     sz = len(content)
50     szbits = (sz & 0x0f) | (_typemap[type]<<4)
51     sz >>= 4
52     while 1:
53         if sz: szbits |= 0x80
54         szout += chr(szbits)
55         if not sz:
56             break
57         szbits = sz & 0x7f
58         sz >>= 7
59     z = zlib.compressobj(1)
60     yield szout
61     yield z.compress(content)
62     yield z.flush()
63
64
65 def _encode_looseobj(type, content):
66     z = zlib.compressobj(1)
67     yield z.compress('%s %d\0' % (type, len(content)))
68     yield z.compress(content)
69     yield z.flush()
70
71
72 def _decode_looseobj(buf):
73     assert(buf);
74     s = zlib.decompress(buf)
75     i = s.find('\0')
76     assert(i > 0)
77     l = s[:i].split(' ')
78     type = l[0]
79     sz = int(l[1])
80     content = s[i+1:]
81     assert(type in _typemap)
82     assert(sz == len(content))
83     return (type, content)
84
85
86 def _decode_packobj(buf):
87     assert(buf)
88     c = ord(buf[0])
89     type = _typermap[(c & 0x70) >> 4]
90     sz = c & 0x0f
91     shift = 4
92     i = 0
93     while c & 0x80:
94         i += 1
95         c = ord(buf[i])
96         sz |= (c & 0x7f) << shift
97         shift += 7
98         if not (c & 0x80):
99             break
100     return (type, zlib.decompress(buf[i+1:]))
101
102
103 class PackIdx:
104     def __init__(self, filename):
105         self.name = filename
106         self.map = mmap_read(open(filename))
107         assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
108         self.fanout = list(struct.unpack('!256I',
109                                          str(buffer(self.map, 8, 256*4))))
110         self.fanout.append(0)  # entry "-1"
111         nsha = self.fanout[255]
112         self.ofstable = buffer(self.map,
113                                8 + 256*4 + nsha*20 + nsha*4,
114                                nsha*4)
115         self.ofs64table = buffer(self.map,
116                                  8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
117
118     def _ofs_from_idx(self, idx):
119         ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
120         if ofs & 0x80000000:
121             idx64 = ofs & 0x7fffffff
122             ofs = struct.unpack('!I',
123                                 str(buffer(self.ofs64table, idx64*8, 8)))[0]
124         return ofs
125
126     def _idx_from_hash(self, hash):
127         assert(len(hash) == 20)
128         b1 = ord(hash[0])
129         start = self.fanout[b1-1] # range -1..254
130         end = self.fanout[b1] # range 0..255
131         buf = buffer(self.map, 8 + 256*4, end*20)
132         want = str(hash)
133         while start < end:
134             mid = start + (end-start)/2
135             v = str(buf[mid*20:(mid+1)*20])
136             if v < want:
137                 start = mid+1
138             elif v > want:
139                 end = mid
140             else: # got it!
141                 return mid
142         return None
143         
144     def find_offset(self, hash):
145         idx = self._idx_from_hash(hash)
146         if idx != None:
147             return self._ofs_from_idx(idx)
148         return None
149
150     def exists(self, hash):
151         return hash and (self._idx_from_hash(hash) != None) and True or None
152
153     def __iter__(self):
154         for i in xrange(self.fanout[255]):
155             yield buffer(self.map, 8 + 256*4 + 20*i, 20)
156
157     def __len__(self):
158         return int(self.fanout[255])
159
160
161 def extract_bits(buf, bits):
162     mask = (1<<bits) - 1
163     v = struct.unpack('!I', buf[0:4])[0]
164     v = (v >> (32-bits)) & mask
165     return v
166
167
168 class PackMidx:
169     def __init__(self, filename):
170         self.name = filename
171         assert(filename.endswith('.midx'))
172         self.map = mmap_read(open(filename))
173         if str(self.map[0:8]) == 'MIDX\0\0\0\1':
174             log('Warning: ignoring old-style midx %r\n' % filename)
175             self.bits = 0
176             self.entries = 1
177             self.fanout = buffer('\0\0\0\0')
178             self.shalist = buffer('\0'*20)
179             self.idxnames = []
180         else:
181             assert(str(self.map[0:8]) == 'MIDX\0\0\0\2')
182             self.bits = struct.unpack('!I', self.map[8:12])[0]
183             self.entries = 2**self.bits
184             self.fanout = buffer(self.map, 12, self.entries*4)
185             shaofs = 12 + self.entries*4
186             nsha = self._fanget(self.entries-1)
187             self.shalist = buffer(self.map, shaofs, nsha*20)
188             self.idxnames = str(self.map[shaofs + 20*nsha:]).split('\0')
189
190     def _fanget(self, i):
191         start = i*4
192         s = self.fanout[start:start+4]
193         return struct.unpack('!I', s)[0]
194     
195     def exists(self, hash):
196         want = str(hash)
197         el = extract_bits(want, self.bits)
198         if el:
199             start = self._fanget(el-1)
200         else:
201             start = 0
202         end = self._fanget(el)
203         while start < end:
204             mid = start + (end-start)/2
205             v = str(self.shalist[mid*20:(mid+1)*20])
206             if v < want:
207                 start = mid+1
208             elif v > want:
209                 end = mid
210             else: # got it!
211                 return True
212         return None
213     
214     def __iter__(self):
215         for i in xrange(self._fanget(self.entries-1)):
216             yield buffer(self.shalist, i*20, 20)
217     
218     def __len__(self):
219         return int(self._fanget(self.entries-1))
220
221
222 _mpi_count = 0
223 class PackIdxList:
224     def __init__(self, dir):
225         global _mpi_count
226         assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
227         _mpi_count += 1
228         self.dir = dir
229         self.also = {}
230         self.packs = []
231         self.refresh()
232
233     def __del__(self):
234         global _mpi_count
235         _mpi_count -= 1
236         assert(_mpi_count == 0)
237
238     def __iter__(self):
239         return iter(idxmerge(self.packs))
240
241     def exists(self, hash):
242         if hash in self.also:
243             return True
244         for i in range(len(self.packs)):
245             p = self.packs[i]
246             if p.exists(hash):
247                 # reorder so most recently used packs are searched first
248                 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
249                 return p.name
250         return None
251
252     def refresh(self, skip_midx = False):
253         skip_midx = skip_midx or ignore_midx
254         d = dict((p.name, p) for p in self.packs
255                  if not skip_midx or not isinstance(p, PackMidx))
256         if os.path.exists(self.dir):
257             if not skip_midx:
258                 midxl = []
259                 for ix in self.packs:
260                     if isinstance(ix, PackMidx):
261                         for name in ix.idxnames:
262                             d[os.path.join(self.dir, name)] = ix
263                 for f in os.listdir(self.dir):
264                     full = os.path.join(self.dir, f)
265                     if f.endswith('.midx') and not d.get(full):
266                         mx = PackMidx(full)
267                         (mxd, mxf) = os.path.split(mx.name)
268                         broken = 0
269                         for n in mx.idxnames:
270                             if not os.path.exists(os.path.join(mxd, n)):
271                                 log(('warning: index %s missing\n' +
272                                     '  used by %s\n') % (n, mxf))
273                                 broken += 1
274                         if not broken:
275                             midxl.append(mx)
276                 midxl.sort(lambda x,y: -cmp(len(x),len(y)))
277                 for ix in midxl:
278                     any = 0
279                     for sub in ix.idxnames:
280                         found = d.get(os.path.join(self.dir, sub))
281                         if not found or isinstance(found, PackIdx):
282                             # doesn't exist, or exists but not in a midx
283                             d[ix.name] = ix
284                             for name in ix.idxnames:
285                                 d[os.path.join(self.dir, name)] = ix
286                             any += 1
287                             break
288                     if not any:
289                         log('midx: removing redundant: %s\n' 
290                             % os.path.basename(ix.name))
291                         unlink(ix.name)
292             for f in os.listdir(self.dir):
293                 full = os.path.join(self.dir, f)
294                 if f.endswith('.idx') and not d.get(full):
295                     ix = PackIdx(full)
296                     d[full] = ix
297             self.packs = list(set(d.values()))
298         log('PackIdxList: using %d index%s.\n' 
299             % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
300
301     def add(self, hash):
302         self.also[hash] = 1
303
304     def zap_also(self):
305         self.also = {}
306
307
308 def calc_hash(type, content):
309     header = '%s %d\0' % (type, len(content))
310     sum = Sha1(header)
311     sum.update(content)
312     return sum.digest()
313
314
315 def _shalist_sort_key(ent):
316     (mode, name, id) = ent
317     if stat.S_ISDIR(int(mode, 8)):
318         return name + '/'
319     else:
320         return name
321
322
323 def idxmerge(idxlist):
324     total = sum(len(i) for i in idxlist)
325     iters = (iter(i) for i in idxlist)
326     heap = [(next(it), it) for it in iters]
327     heapq.heapify(heap)
328     count = 0
329     last = None
330     while heap:
331         if (count % 10024) == 0:
332             progress('Reading indexes: %.2f%% (%d/%d)\r'
333                      % (count*100.0/total, count, total))
334         (e, it) = heap[0]
335         if e != last:
336             yield e
337             last = e
338         count += 1
339         e = next(it)
340         if e:
341             heapq.heapreplace(heap, (e, it))
342         else:
343             heapq.heappop(heap)
344     log('Reading indexes: %.2f%% (%d/%d), done.\n' % (100, total, total))
345
346     
347 class PackWriter:
348     def __init__(self, objcache_maker=None):
349         self.count = 0
350         self.outbytes = 0
351         self.filename = None
352         self.file = None
353         self.objcache_maker = objcache_maker
354         self.objcache = None
355
356     def __del__(self):
357         self.close()
358
359     def _make_objcache(self):
360         if not self.objcache:
361             if self.objcache_maker:
362                 self.objcache = self.objcache_maker()
363             else:
364                 self.objcache = PackIdxList(repo('objects/pack'))
365
366     def _open(self):
367         if not self.file:
368             self._make_objcache()
369             (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
370             self.file = os.fdopen(fd, 'w+b')
371             assert(name.endswith('.pack'))
372             self.filename = name[:-5]
373             self.file.write('PACK\0\0\0\2\0\0\0\0')
374
375     def _raw_write(self, datalist):
376         self._open()
377         f = self.file
378         # in case we get interrupted (eg. KeyboardInterrupt), it's best if
379         # the file never has a *partial* blob.  So let's make sure it's
380         # all-or-nothing.  (The blob shouldn't be very big anyway, thanks
381         # to our hashsplit algorithm.)  f.write() does its own buffering,
382         # but that's okay because we'll flush it in _end().
383         oneblob = ''.join(datalist)
384         f.write(oneblob)
385         self.outbytes += len(oneblob)
386         self.count += 1
387
388     def _write(self, bin, type, content):
389         if verbose:
390             log('>')
391         self._raw_write(_encode_packobj(type, content))
392         return bin
393
394     def breakpoint(self):
395         id = self._end()
396         self.outbytes = self.count = 0
397         return id
398
399     def write(self, type, content):
400         return self._write(calc_hash(type, content), type, content)
401
402     def exists(self, id):
403         if not self.objcache:
404             self._make_objcache()
405         return self.objcache.exists(id)
406
407     def maybe_write(self, type, content):
408         bin = calc_hash(type, content)
409         if not self.exists(bin):
410             self._write(bin, type, content)
411             self.objcache.add(bin)
412         return bin
413
414     def new_blob(self, blob):
415         return self.maybe_write('blob', blob)
416
417     def new_tree(self, shalist):
418         shalist = sorted(shalist, key = _shalist_sort_key)
419         l = []
420         for (mode,name,bin) in shalist:
421             assert(mode)
422             assert(mode != '0')
423             assert(mode[0] != '0')
424             assert(name)
425             assert(len(bin) == 20)
426             l.append('%s %s\0%s' % (mode,name,bin))
427         return self.maybe_write('tree', ''.join(l))
428
429     def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
430         l = []
431         if tree: l.append('tree %s' % tree.encode('hex'))
432         if parent: l.append('parent %s' % parent.encode('hex'))
433         if author: l.append('author %s %s' % (author, _git_date(adate)))
434         if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
435         l.append('')
436         l.append(msg)
437         return self.maybe_write('commit', '\n'.join(l))
438
439     def new_commit(self, parent, tree, msg):
440         now = time.time()
441         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
442         commit = self._new_commit(tree, parent,
443                                   userline, now, userline, now,
444                                   msg)
445         return commit
446
447     def abort(self):
448         f = self.file
449         if f:
450             self.file = None
451             f.close()
452             os.unlink(self.filename + '.pack')
453
454     def _end(self):
455         f = self.file
456         if not f: return None
457         self.file = None
458         self.objcache = None
459
460         # update object count
461         f.seek(8)
462         cp = struct.pack('!i', self.count)
463         assert(len(cp) == 4)
464         f.write(cp)
465
466         # calculate the pack sha1sum
467         f.seek(0)
468         sum = Sha1()
469         while 1:
470             b = f.read(65536)
471             sum.update(b)
472             if not b: break
473         f.write(sum.digest())
474         
475         f.close()
476
477         p = subprocess.Popen(['git', 'index-pack', '-v',
478                               '--index-version=2',
479                               self.filename + '.pack'],
480                              preexec_fn = _gitenv,
481                              stdout = subprocess.PIPE)
482         out = p.stdout.read().strip()
483         _git_wait('git index-pack', p)
484         if not out:
485             raise GitError('git index-pack produced no output')
486         nameprefix = repo('objects/pack/%s' % out)
487         if os.path.exists(self.filename + '.map'):
488             os.unlink(self.filename + '.map')
489         os.rename(self.filename + '.pack', nameprefix + '.pack')
490         os.rename(self.filename + '.idx', nameprefix + '.idx')
491         return nameprefix
492
493     def close(self):
494         return self._end()
495
496
497 def _git_date(date):
498     return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
499
500
501 def _gitenv():
502     os.environ['GIT_DIR'] = os.path.abspath(repo())
503
504
505 def list_refs(refname = None):
506     argv = ['git', 'show-ref', '--']
507     if refname:
508         argv += [refname]
509     p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
510     out = p.stdout.read().strip()
511     rv = p.wait()  # not fatal
512     if rv:
513         assert(not out)
514     if out:
515         for d in out.split('\n'):
516             (sha, name) = d.split(' ', 1)
517             yield (name, sha.decode('hex'))
518
519
520 def read_ref(refname):
521     l = list(list_refs(refname))
522     if l:
523         assert(len(l) == 1)
524         return l[0][1]
525     else:
526         return None
527
528
529 def rev_list(ref, count=None):
530     assert(not ref.startswith('-'))
531     opts = []
532     if count:
533         opts += ['-n', str(atoi(count))]
534     argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
535     p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
536     commit = None
537     for row in p.stdout:
538         s = row.strip()
539         if s.startswith('commit '):
540             commit = s[7:].decode('hex')
541         else:
542             date = int(s)
543             yield (date, commit)
544     rv = p.wait()  # not fatal
545     if rv:
546         raise GitError, 'git rev-list returned error %d' % rv
547
548
549 def rev_get_date(ref):
550     for (date, commit) in rev_list(ref, count=1):
551         return date
552     raise GitError, 'no such commit %r' % ref
553
554
555 def update_ref(refname, newval, oldval):
556     if not oldval:
557         oldval = ''
558     assert(refname.startswith('refs/heads/'))
559     p = subprocess.Popen(['git', 'update-ref', refname,
560                           newval.encode('hex'), oldval.encode('hex')],
561                          preexec_fn = _gitenv)
562     _git_wait('git update-ref', p)
563
564
565 def guess_repo(path=None):
566     global repodir
567     if path:
568         repodir = path
569     if not repodir:
570         repodir = os.environ.get('BUP_DIR')
571         if not repodir:
572             repodir = os.path.expanduser('~/.bup')
573
574
575 def init_repo(path=None):
576     guess_repo(path)
577     d = repo()
578     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
579         raise GitError('"%d" exists but is not a directory\n' % d)
580     p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
581                          preexec_fn = _gitenv)
582     _git_wait('git init', p)
583     p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
584                          stdout=sys.stderr, preexec_fn = _gitenv)
585     _git_wait('git config', p)
586
587
588 def check_repo_or_die(path=None):
589     guess_repo(path)
590     if not os.path.isdir(repo('objects/pack/.')):
591         if repodir == home_repodir:
592             init_repo()
593         else:
594             log('error: %r is not a bup/git repository\n' % repo())
595             sys.exit(15)
596
597
598 def _treeparse(buf):
599     ofs = 0
600     while ofs < len(buf):
601         z = buf[ofs:].find('\0')
602         assert(z > 0)
603         spl = buf[ofs:ofs+z].split(' ', 1)
604         assert(len(spl) == 2)
605         sha = buf[ofs+z+1:ofs+z+1+20]
606         ofs += z+1+20
607         yield (spl[0], spl[1], sha)
608
609
610 _ver = None
611 def ver():
612     global _ver
613     if not _ver:
614         p = subprocess.Popen(['git', '--version'],
615                              stdout=subprocess.PIPE)
616         gvs = p.stdout.read()
617         _git_wait('git --version', p)
618         m = re.match(r'git version (\S+.\S+)', gvs)
619         if not m:
620             raise GitError('git --version weird output: %r' % gvs)
621         _ver = tuple(m.group(1).split('.'))
622     needed = ('1','5', '3', '1')
623     if _ver < needed:
624         raise GitError('git version %s or higher is required; you have %s'
625                        % ('.'.join(needed), '.'.join(_ver)))
626     return _ver
627
628
629 def _git_wait(cmd, p):
630     rv = p.wait()
631     if rv != 0:
632         raise GitError('%s returned %d' % (cmd, rv))
633
634
635 def _git_capture(argv):
636     p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
637     r = p.stdout.read()
638     _git_wait(repr(argv), p)
639     return r
640
641
642 class AbortableIter:
643     def __init__(self, it, onabort = None):
644         self.it = it
645         self.onabort = onabort
646         self.done = None
647
648     def __iter__(self):
649         return self
650         
651     def next(self):
652         try:
653             return self.it.next()
654         except StopIteration, e:
655             self.done = True
656             raise
657         except:
658             self.abort()
659             raise
660
661     def abort(self):
662         if not self.done:
663             self.done = True
664             if self.onabort:
665                 self.onabort()
666         
667     def __del__(self):
668         self.abort()
669
670
671 _ver_warned = 0
672 class CatPipe:
673     def __init__(self):
674         global _ver_warned
675         wanted = ('1','5','6')
676         if ver() < wanted:
677             if not _ver_warned:
678                 log('warning: git version < %s; bup will be slow.\n'
679                     % '.'.join(wanted))
680                 _ver_warned = 1
681             self.get = self._slow_get
682         else:
683             self.p = self.inprogress = None
684             self.get = self._fast_get
685
686     def _abort(self):
687         if self.p:
688             self.p.stdout.close()
689             self.p.stdin.close()
690         self.p = None
691         self.inprogress = None
692
693     def _restart(self):
694         self._abort()
695         self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
696                                   stdin=subprocess.PIPE, 
697                                   stdout=subprocess.PIPE,
698                                   preexec_fn = _gitenv)
699
700     def _fast_get(self, id):
701         if not self.p or self.p.poll() != None:
702             self._restart()
703         assert(self.p)
704         assert(self.p.poll() == None)
705         if self.inprogress:
706             log('_fast_get: opening %r while %r is open' 
707                 % (id, self.inprogress))
708         assert(not self.inprogress)
709         assert(id.find('\n') < 0)
710         assert(id.find('\r') < 0)
711         assert(id[0] != '-')
712         self.inprogress = id
713         self.p.stdin.write('%s\n' % id)
714         hdr = self.p.stdout.readline()
715         if hdr.endswith(' missing\n'):
716             raise KeyError('blob %r is missing' % id)
717         spl = hdr.split(' ')
718         if len(spl) != 3 or len(spl[0]) != 40:
719             raise GitError('expected blob, got %r' % spl)
720         (hex, type, size) = spl
721
722         it = AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
723                            onabort = self._abort)
724         try:
725             yield type
726             for blob in it:
727                 yield blob
728             assert(self.p.stdout.readline() == '\n')
729             self.inprogress = None
730         except Exception, e:
731             it.abort()
732             raise
733
734     def _slow_get(self, id):
735         assert(id.find('\n') < 0)
736         assert(id.find('\r') < 0)
737         assert(id[0] != '-')
738         type = _git_capture(['git', 'cat-file', '-t', id]).strip()
739         yield type
740
741         p = subprocess.Popen(['git', 'cat-file', type, id],
742                              stdout=subprocess.PIPE,
743                              preexec_fn = _gitenv)
744         for blob in chunkyreader(p.stdout):
745             yield blob
746         _git_wait('git cat-file', p)
747
748     def _join(self, it):
749         type = it.next()
750         if type == 'blob':
751             for blob in it:
752                 yield blob
753         elif type == 'tree':
754             treefile = ''.join(it)
755             for (mode, name, sha) in _treeparse(treefile):
756                 for blob in self.join(sha.encode('hex')):
757                     yield blob
758         elif type == 'commit':
759             treeline = ''.join(it).split('\n')[0]
760             assert(treeline.startswith('tree '))
761             for blob in self.join(treeline[5:]):
762                 yield blob
763         else:
764             raise GitError('invalid object type %r: expected blob/tree/commit'
765                            % type)
766
767     def join(self, id):
768         try:
769             for d in self._join(self.get(id)):
770                 yield d
771         except StopIteration:
772             log('booger!\n')
773         
774
775 def cat(id):
776     c = CatPipe()
777     for d in c.join(id):
778         yield d