]> arthur.barton.de Git - bup.git/blob - git.py
Refactored client stuff into client.py; now cmd-save and cmd-init use it too.
[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)
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, ref, tree, msg):
198         now = time.time()
199         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
200         oldref = ref and _read_ref(ref) or None
201         commit = self._new_commit(tree, oldref,
202                                   userline, now, userline, now,
203                                   msg)
204         if ref:
205             self.close()  # UGLY: needed so _update_ref can see the new objects
206             _update_ref(ref, commit.encode('hex'), oldref)
207         return commit
208
209     def abort(self):
210         f = self.file
211         if f:
212             self.file = None
213             f.close()
214             os.unlink(self.filename + '.pack')
215
216     def close(self):
217         f = self.file
218         if not f: return None
219         self.file = None
220
221         # update object count
222         f.seek(8)
223         cp = struct.pack('!i', self.count)
224         assert(len(cp) == 4)
225         f.write(cp)
226
227         # calculate the pack sha1sum
228         f.seek(0)
229         sum = sha.sha()
230         while 1:
231             b = f.read(65536)
232             sum.update(b)
233             if not b: break
234         f.write(sum.digest())
235         
236         f.close()
237
238         p = subprocess.Popen(['git', 'index-pack', '-v',
239                               self.filename + '.pack'],
240                              preexec_fn = _gitenv,
241                              stdout = subprocess.PIPE)
242         out = p.stdout.read().strip()
243         if p.wait() or not out:
244             raise GitError('git index-pack returned an error')
245         nameprefix = repo('objects/pack/%s' % out)
246         os.rename(self.filename + '.pack', nameprefix + '.pack')
247         os.rename(self.filename + '.idx', nameprefix + '.idx')
248         return nameprefix
249
250
251 class PackWriter_Remote(PackWriter):
252     def __init__(self, conn, objcache=None, onclose=None):
253         PackWriter.__init__(self, objcache)
254         self.file = conn
255         self.filename = 'remote socket'
256         self.onclose = onclose
257
258     def _open(self):
259         assert(not "can't reopen a PackWriter_Remote")
260
261     def close(self):
262         if self.file:
263             self.file.write('\0\0\0\0')
264             if self.onclose:
265                 self.onclose()
266         self.file = None
267
268     def abort(self):
269         raise GitError("don't know how to abort remote pack writing")
270
271     def _raw_write(self, datalist):
272         assert(self.file)
273         data = ''.join(datalist)
274         assert(len(data))
275         self.file.write(struct.pack('!I', len(data)) + data)
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     p.wait()
292     if out:
293         return out.split()[0]
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, newval, oldval],
302                          preexec_fn = _gitenv)
303     p.wait()
304     return newval
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', 'init', '--bare'], stdout=sys.stderr,
323                          preexec_fn = _gitenv)
324     return p.wait()
325
326
327 def check_repo_or_die(path=None):
328     guess_repo(path)
329     if not os.path.isdir(repo('objects/pack/.')):
330         if repodir == home_repodir:
331             init_repo()
332         else:
333             log('error: %r is not a bup/git repository\n' % repo())
334             exit(15)