]> arthur.barton.de Git - bup.git/blob - git.py
Makefile: avoid using backquotes.
[bup.git] / git.py
1 import os, errno, zlib, time, sha, subprocess, struct, mmap, stat
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         if p.wait() or not out:
241             raise GitError('git index-pack returned an error')
242         nameprefix = repo('objects/pack/%s' % out)
243         os.rename(self.filename + '.pack', nameprefix + '.pack')
244         os.rename(self.filename + '.idx', nameprefix + '.idx')
245         return nameprefix
246
247
248 class PackWriter_Remote(PackWriter):
249     def __init__(self, conn, objcache=None, onclose=None):
250         PackWriter.__init__(self, objcache)
251         self.file = conn
252         self.filename = 'remote socket'
253         self.onclose = onclose
254
255     def _open(self):
256         assert(not "can't reopen a PackWriter_Remote")
257
258     def close(self):
259         if self.file:
260             self.file.write('\0\0\0\0')
261             if self.onclose:
262                 self.onclose()
263         self.file = None
264
265     def abort(self):
266         raise GitError("don't know how to abort remote pack writing")
267
268     def _raw_write(self, datalist):
269         assert(self.file)
270         data = ''.join(datalist)
271         assert(len(data))
272         self.file.write(struct.pack('!I', len(data)) + data)
273
274
275 def _git_date(date):
276     return time.strftime('%s %z', time.localtime(date))
277
278
279 def _gitenv():
280     os.environ['GIT_DIR'] = os.path.abspath(repo())
281
282
283 def read_ref(refname):
284     p = subprocess.Popen(['git', 'show-ref', '--', refname],
285                          preexec_fn = _gitenv,
286                          stdout = subprocess.PIPE)
287     out = p.stdout.read().strip()
288     rv = p.wait()
289     if rv:
290         assert(not out)
291     if out:
292         return out.split()[0].decode('hex')
293     else:
294         return None
295
296
297 def update_ref(refname, newval, oldval):
298     if not oldval:
299         oldval = ''
300     p = subprocess.Popen(['git', 'update-ref', '--', refname,
301                           newval.encode('hex'), oldval.encode('hex')],
302                          preexec_fn = _gitenv)
303     rv = p.wait()
304     if rv:
305         raise GitError('update_ref returned error code %d' % rv)
306
307
308 def guess_repo(path=None):
309     global repodir
310     if path:
311         repodir = path
312     if not repodir:
313         repodir = os.environ.get('BUP_DIR')
314         if not repodir:
315             repodir = os.path.expanduser('~/.bup')
316
317
318 def init_repo(path=None):
319     guess_repo(path)
320     d = repo()
321     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
322         raise GitError('"%d" exists but is not a directory\n' % d)
323     p = subprocess.Popen(['git', 'init', '--bare'], stdout=sys.stderr,
324                          preexec_fn = _gitenv)
325     return p.wait()
326
327
328 def check_repo_or_die(path=None):
329     guess_repo(path)
330     if not os.path.isdir(repo('objects/pack/.')):
331         if repodir == home_repodir:
332             init_repo()
333         else:
334             log('error: %r is not a bup/git repository\n' % repo())
335             exit(15)
336
337
338 def _treeparse(buf):
339     ofs = 0
340     while ofs < len(buf):
341         z = buf[ofs:].find('\0')
342         assert(z > 0)
343         spl = buf[ofs:ofs+z].split(' ', 1)
344         assert(len(spl) == 2)
345         sha = buf[ofs+z+1:ofs+z+1+20]
346         ofs += z+1+20
347         yield (spl[0], spl[1], sha)
348
349
350 class CatPipe:
351     def __init__(self):
352         self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
353                                   stdin=subprocess.PIPE, 
354                                   stdout=subprocess.PIPE,
355                                   preexec_fn = _gitenv)
356
357     def get(self, id):
358         assert(id.find('\n') < 0)
359         assert(id.find('\r') < 0)
360         self.p.stdin.write('%s\n' % id)
361         hdr = self.p.stdout.readline()
362         spl = hdr.split(' ')
363         assert(len(spl) == 3)
364         assert(len(spl[0]) == 40)
365         (hex, type, size) = spl
366         yield type
367         for blob in chunkyreader(self.p.stdout, int(spl[2])):
368             yield blob
369         assert(self.p.stdout.readline() == '\n')
370
371     def _join(self, it):
372         type = it.next()
373         if type == 'blob':
374             for blob in it:
375                 yield blob
376         elif type == 'tree':
377             treefile = ''.join(it)
378             for (mode, name, sha) in _treeparse(treefile):
379                 for blob in self.join(sha.encode('hex')):
380                     yield blob
381         elif type == 'commit':
382             treeline = ''.join(it).split('\n')[0]
383             assert(treeline.startswith('tree '))
384             for blob in self.join(treeline[5:]):
385                 yield blob
386         else:
387             raise GitError('unknown object type %r' % type)
388
389     def join(self, id):
390         for d in self._join(self.get(id)):
391             yield d
392         
393
394 def cat(id):
395     c = CatPipe()
396     for d in c.join(id):
397         yield d