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