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