]> arthur.barton.de Git - bup.git/blob - git.py
ccafa5c8b7e8eedd6e842d51cd70e78385386dfe
[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 maybe_write(self, type, content):
187         bin = calc_hash(type, content)
188         if not self.objcache:
189             self._make_objcache()
190         if not self.objcache.exists(bin):
191             self._write(bin, type, content)
192             self.objcache.add(bin)
193         return bin
194
195     def new_blob(self, blob):
196         return self.maybe_write('blob', blob)
197
198     def new_tree(self, shalist):
199         shalist = sorted(shalist, key = _shalist_sort_key)
200         l = ['%s %s\0%s' % (mode,name,bin) 
201              for (mode,name,bin) in shalist]
202         return self.maybe_write('tree', ''.join(l))
203
204     def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
205         l = []
206         if tree: l.append('tree %s' % tree.encode('hex'))
207         if parent: l.append('parent %s' % parent.encode('hex'))
208         if author: l.append('author %s %s' % (author, _git_date(adate)))
209         if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
210         l.append('')
211         l.append(msg)
212         return self.maybe_write('commit', '\n'.join(l))
213
214     def new_commit(self, parent, tree, msg):
215         now = time.time()
216         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
217         commit = self._new_commit(tree, parent,
218                                   userline, now, userline, now,
219                                   msg)
220         return commit
221
222     def abort(self):
223         f = self.file
224         if f:
225             self.file = None
226             f.close()
227             os.unlink(self.filename + '.pack')
228
229     def _end(self):
230         f = self.file
231         if not f: return None
232         self.file = None
233
234         # update object count
235         f.seek(8)
236         cp = struct.pack('!i', self.count)
237         assert(len(cp) == 4)
238         f.write(cp)
239
240         # calculate the pack sha1sum
241         f.seek(0)
242         sum = sha.sha()
243         while 1:
244             b = f.read(65536)
245             sum.update(b)
246             if not b: break
247         f.write(sum.digest())
248         
249         f.close()
250         self.objcache = None
251
252         p = subprocess.Popen(['git', 'index-pack', '-v',
253                               '--index-version=2',
254                               self.filename + '.pack'],
255                              preexec_fn = _gitenv,
256                              stdout = subprocess.PIPE)
257         out = p.stdout.read().strip()
258         _git_wait('git index-pack', p)
259         if not out:
260             raise GitError('git index-pack produced no output')
261         nameprefix = repo('objects/pack/%s' % out)
262         os.rename(self.filename + '.pack', nameprefix + '.pack')
263         os.rename(self.filename + '.idx', nameprefix + '.idx')
264         return nameprefix
265
266     def close(self):
267         return self._end()
268
269
270 def _git_date(date):
271     return time.strftime('%s %z', time.localtime(date))
272
273
274 def _gitenv():
275     os.environ['GIT_DIR'] = os.path.abspath(repo())
276
277
278 def read_ref(refname):
279     p = subprocess.Popen(['git', 'show-ref', '--', refname],
280                          preexec_fn = _gitenv,
281                          stdout = subprocess.PIPE)
282     out = p.stdout.read().strip()
283     rv = p.wait()  # not fatal
284     if rv:
285         assert(not out)
286     if out:
287         return out.split()[0].decode('hex')
288     else:
289         return None
290
291
292 def update_ref(refname, newval, oldval):
293     if not oldval:
294         oldval = ''
295     p = subprocess.Popen(['git', 'update-ref', '--', refname,
296                           newval.encode('hex'), oldval.encode('hex')],
297                          preexec_fn = _gitenv)
298     _git_wait('git update-ref', p)
299
300
301 def guess_repo(path=None):
302     global repodir
303     if path:
304         repodir = path
305     if not repodir:
306         repodir = os.environ.get('BUP_DIR')
307         if not repodir:
308             repodir = os.path.expanduser('~/.bup')
309
310
311 def init_repo(path=None):
312     guess_repo(path)
313     d = repo()
314     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
315         raise GitError('"%d" exists but is not a directory\n' % d)
316     p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
317                          preexec_fn = _gitenv)
318     _git_wait('git init', p)
319     p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
320                          stdout=sys.stderr, preexec_fn = _gitenv)
321     _git_wait('git config', p)
322
323
324 def check_repo_or_die(path=None):
325     guess_repo(path)
326     if not os.path.isdir(repo('objects/pack/.')):
327         if repodir == home_repodir:
328             init_repo()
329         else:
330             log('error: %r is not a bup/git repository\n' % repo())
331             exit(15)
332
333
334 def _treeparse(buf):
335     ofs = 0
336     while ofs < len(buf):
337         z = buf[ofs:].find('\0')
338         assert(z > 0)
339         spl = buf[ofs:ofs+z].split(' ', 1)
340         assert(len(spl) == 2)
341         sha = buf[ofs+z+1:ofs+z+1+20]
342         ofs += z+1+20
343         yield (spl[0], spl[1], sha)
344
345 _ver = None
346 def ver():
347     global _ver
348     if not _ver:
349         p = subprocess.Popen(['git', '--version'],
350                              stdout=subprocess.PIPE)
351         gvs = p.stdout.read()
352         _git_wait('git --version', p)
353         m = re.match(r'git version (\S+.\S+)', gvs)
354         if not m:
355             raise GitError('git --version weird output: %r' % gvs)
356         _ver = tuple(m.group(1).split('.'))
357     needed = ('1','5','4')
358     if _ver < needed:
359         raise GitError('git version %s or higher is required; you have %s'
360                        % ('.'.join(needed), '.'.join(_ver)))
361     return _ver
362
363
364 def _git_wait(cmd, p):
365     rv = p.wait()
366     if rv != 0:
367         raise GitError('%s returned %d' % (cmd, rv))
368
369
370 def _git_capture(argv):
371     p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
372     r = p.stdout.read()
373     _git_wait(repr(argv), p)
374     return r
375
376
377 _ver_warned = 0
378 class CatPipe:
379     def __init__(self):
380         global _ver_warned
381         wanted = ('1','5','6')
382         if ver() < wanted:
383             if not _ver_warned:
384                 log('warning: git version < %s; bup will be slow.\n'
385                     % '.'.join(wanted))
386                 _ver_warned = 1
387             self.get = self._slow_get
388         else:
389             self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
390                                       stdin=subprocess.PIPE, 
391                                       stdout=subprocess.PIPE,
392                                       preexec_fn = _gitenv)
393             self.get = self._fast_get
394
395     def _fast_get(self, id):
396         assert(id.find('\n') < 0)
397         assert(id.find('\r') < 0)
398         assert(id[0] != '-')
399         self.p.stdin.write('%s\n' % id)
400         hdr = self.p.stdout.readline()
401         spl = hdr.split(' ')
402         assert(len(spl) == 3)
403         assert(len(spl[0]) == 40)
404         (hex, type, size) = spl
405         yield type
406         for blob in chunkyreader(self.p.stdout, int(spl[2])):
407             yield blob
408         assert(self.p.stdout.readline() == '\n')
409
410     def _slow_get(self, id):
411         assert(id.find('\n') < 0)
412         assert(id.find('\r') < 0)
413         assert(id[0] != '-')
414         type = _git_capture(['git', 'cat-file', '-t', id]).strip()
415         yield type
416
417         p = subprocess.Popen(['git', 'cat-file', type, id],
418                              stdout=subprocess.PIPE,
419                              preexec_fn = _gitenv)
420         for blob in chunkyreader(p.stdout):
421             yield blob
422         _git_wait('git cat-file', p)
423
424     def _join(self, it):
425         type = it.next()
426         if type == 'blob':
427             for blob in it:
428                 yield blob
429         elif type == 'tree':
430             treefile = ''.join(it)
431             for (mode, name, sha) in _treeparse(treefile):
432                 for blob in self.join(sha.encode('hex')):
433                     yield blob
434         elif type == 'commit':
435             treeline = ''.join(it).split('\n')[0]
436             assert(treeline.startswith('tree '))
437             for blob in self.join(treeline[5:]):
438                 yield blob
439         else:
440             raise GitError('unknown object type %r' % type)
441
442     def join(self, id):
443         for d in self._join(self.get(id)):
444             yield d
445         
446
447 def cat(id):
448     c = CatPipe()
449     for d in c.join(id):
450         yield d