]> arthur.barton.de Git - bup.git/blob - git.py
Fix some problems running on older Debian.
[bup.git] / git.py
1 import os, errno, zlib, time, sha, subprocess, struct, mmap, stat
2 from helpers import *
3
4 verbose = 0
5 repodir = os.environ.get('BUP_DIR', '.git')
6
7 def repo(sub = ''):
8     global repodir
9     gd = os.path.join(repodir, '.git')
10     if os.path.exists(gd):
11         repodir = gd
12     return os.path.join(repodir, sub)
13
14
15 class PackIndex:
16     def __init__(self, filename):
17         self.name = filename
18         f = open(filename)
19         self.map = mmap.mmap(f.fileno(), 0,
20                              mmap.MAP_SHARED, mmap.PROT_READ)
21         f.close()  # map will persist beyond file close
22         assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
23         self.fanout = list(struct.unpack('!256I',
24                                          str(buffer(self.map, 8, 256*4))))
25         self.fanout.append(0)  # entry "-1"
26         nsha = self.fanout[255]
27         self.ofstable = buffer(self.map,
28                                8 + 256*4 + nsha*20 + nsha*4,
29                                nsha*4)
30         self.ofs64table = buffer(self.map,
31                                  8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
32
33     def _ofs_from_idx(self, idx):
34         ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
35         if ofs & 0x80000000:
36             idx64 = ofs & 0x7fffffff
37             ofs = struct.unpack('!I',
38                                 str(buffer(self.ofs64table, idx64*8, 8)))[0]
39         return ofs
40
41     def _idx_from_hash(self, hash):
42         assert(len(hash) == 20)
43         b1 = ord(hash[0])
44         start = self.fanout[b1-1] # range -1..254
45         end = self.fanout[b1] # range 0..255
46         buf = buffer(self.map, 8 + 256*4, end*20)
47         want = buffer(hash)
48         while start < end:
49             mid = start + (end-start)/2
50             v = buffer(buf, mid*20, 20)
51             if v < want:
52                 start = mid+1
53             elif v > want:
54                 end = mid
55             else: # got it!
56                 return mid
57         return None
58         
59     def find_offset(self, hash):
60         idx = self._idx_from_hash(hash)
61         if idx != None:
62             return self._ofs_from_idx(idx)
63         return None
64
65     def exists(self, hash):
66         return (self._idx_from_hash(hash) != None) and True or None
67
68
69 class MultiPackIndex:
70     def __init__(self, dir):
71         self.packs = []
72         self.also = {}
73         for f in os.listdir(dir):
74             if f.endswith('.idx'):
75                 self.packs.append(PackIndex(os.path.join(dir, f)))
76
77     def exists(self, hash):
78         if hash in self.also:
79             return True
80         for i in range(len(self.packs)):
81             p = self.packs[i]
82             if p.exists(hash):
83                 # reorder so most recently used packs are searched first
84                 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
85                 return True
86         return None
87
88     def add(self, hash):
89         self.also[hash] = 1
90
91     def zap_also(self):
92         self.also = {}
93
94
95 def calc_hash(type, content):
96     header = '%s %d\0' % (type, len(content))
97     sum = sha.sha(header)
98     sum.update(content)
99     return sum.digest()
100
101
102 def _shalist_sort_key(ent):
103     (mode, name, id) = ent
104     if stat.S_ISDIR(int(mode, 8)):
105         return name + '/'
106     else:
107         return name
108
109
110 _typemap = dict(blob=3, tree=2, commit=1, tag=8)
111 class PackWriter:
112     def __init__(self, objcache=None):
113         self.count = 0
114         self.filename = None
115         self.file = None
116         self.objcache = objcache or MultiPackIndex(repo('objects/pack'))
117
118     def __del__(self):
119         self.close()
120
121     def _open(self):
122         assert(not self.file)
123         self.objcache.zap_also()
124         self.filename = repo('objects/bup%d' % os.getpid())
125         self.file = open(self.filename + '.pack', 'w+')
126         self.file.write('PACK\0\0\0\2\0\0\0\0')
127
128     def _raw_write(self, datalist):
129         if not self.file:
130             self._open()
131         f = self.file
132         for d in datalist:
133             f.write(d)
134         self.count += 1
135
136     def _write(self, bin, type, content):
137         if verbose:
138             log('>')
139
140         out = []
141
142         sz = len(content)
143         szbits = (sz & 0x0f) | (_typemap[type]<<4)
144         sz >>= 4
145         while 1:
146             if sz: szbits |= 0x80
147             out.append(chr(szbits))
148             if not sz:
149                 break
150             szbits = sz & 0x7f
151             sz >>= 7
152
153         z = zlib.compressobj(1)
154         out.append(z.compress(content))
155         out.append(z.flush())
156
157         self._raw_write(out)
158         return bin
159
160     def write(self, type, content):
161         return self._write(calc_hash(type, content), type, content)
162
163     def maybe_write(self, type, content):
164         bin = calc_hash(type, content)
165         if not self.objcache.exists(bin):
166             self._write(bin, type, content)
167             self.objcache.add(bin)
168         return bin
169
170     def new_blob(self, blob):
171         return self.maybe_write('blob', blob)
172
173     def new_tree(self, shalist):
174         shalist = sorted(shalist, key = _shalist_sort_key)
175         l = ['%s %s\0%s' % (mode,name,bin) 
176              for (mode,name,bin) in shalist]
177         return self.maybe_write('tree', ''.join(l))
178
179     def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
180         l = []
181         if tree: l.append('tree %s' % tree.encode('hex'))
182         if parent: l.append('parent %s' % parent)
183         if author: l.append('author %s %s' % (author, _git_date(adate)))
184         if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
185         l.append('')
186         l.append(msg)
187         return self.maybe_write('commit', '\n'.join(l))
188
189     def new_commit(self, ref, tree, msg):
190         now = time.time()
191         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
192         oldref = ref and _read_ref(ref) or None
193         commit = self._new_commit(tree, oldref,
194                                   userline, now, userline, now,
195                                   msg)
196         if ref:
197             self.close()  # UGLY: needed so _update_ref can see the new objects
198             _update_ref(ref, commit.encode('hex'), oldref)
199         return commit
200
201     def abort(self):
202         f = self.file
203         if f:
204             self.file = None
205             f.close()
206             os.unlink(self.filename + '.pack')
207
208     def close(self):
209         f = self.file
210         if not f: return None
211         self.file = None
212
213         # update object count
214         f.seek(8)
215         cp = struct.pack('!i', self.count)
216         assert(len(cp) == 4)
217         f.write(cp)
218
219         # calculate the pack sha1sum
220         f.seek(0)
221         sum = sha.sha()
222         while 1:
223             b = f.read(65536)
224             sum.update(b)
225             if not b: break
226         f.write(sum.digest())
227         
228         f.close()
229
230         p = subprocess.Popen(['git', 'index-pack', '-v',
231                               self.filename + '.pack'],
232                              preexec_fn = _gitenv,
233                              stdout = subprocess.PIPE)
234         out = p.stdout.read().strip()
235         if p.wait() or not out:
236             raise Exception('git index-pack returned an error')
237         nameprefix = repo('objects/pack/%s' % out)
238         os.rename(self.filename + '.pack', nameprefix + '.pack')
239         os.rename(self.filename + '.idx', nameprefix + '.idx')
240         return nameprefix
241
242
243 class PackWriter_Remote(PackWriter):
244     def __init__(self, conn, objcache=None):
245         PackWriter.__init__(self, objcache)
246         self.file = conn
247         self.filename = 'remote socket'
248
249     def _open(self):
250         assert(not "can't reopen a PackWriter_Remote")
251
252     def close(self):
253         if self.file:
254             self.file.write('\0\0\0\0')
255         self.file = None
256
257     def _raw_write(self, datalist):
258         assert(self.file)
259         data = ''.join(datalist)
260         assert(len(data))
261         self.file.write(struct.pack('!I', len(data)) + data)
262
263
264 def _git_date(date):
265     return time.strftime('%s %z', time.localtime(date))
266
267
268 def _gitenv():
269     os.environ['GIT_DIR'] = os.path.abspath(repo())
270
271
272 def _read_ref(refname):
273     p = subprocess.Popen(['git', 'show-ref', '--', refname],
274                          preexec_fn = _gitenv,
275                          stdout = subprocess.PIPE)
276     out = p.stdout.read().strip()
277     p.wait()
278     if out:
279         return out.split()[0]
280     else:
281         return None
282
283
284 def _update_ref(refname, newval, oldval):
285     if not oldval:
286         oldval = ''
287     p = subprocess.Popen(['git', 'update-ref', '--', refname, newval, oldval],
288                          preexec_fn = _gitenv)
289     p.wait()
290     return newval
291
292
293 def init_repo():
294     d = repo()
295     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
296         raise Exception('"%d" exists but is not a directory\n' % d)
297     p = subprocess.Popen(['git', 'init', '--bare'],
298                          preexec_fn = _gitenv)
299     return p.wait()
300
301
302 def check_repo_or_die():
303     if not os.path.isdir(repo('objects/pack/.')):
304         log('error: %r is not a bup/git repository\n' % repo())
305         exit(15)