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