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