]> arthur.barton.de Git - bup.git/blob - git.py
bup save: handle symlinks correctly.
[bup.git] / git.py
1 import os, errno, zlib, time, sha, subprocess, struct, mmap, stat
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 def _shalist_sort_key(ent):
96     (mode, name, id) = ent
97     if stat.S_ISDIR(int(mode, 8)):
98         return name + '/'
99     else:
100         return name
101
102
103 _typemap = dict(blob=3, tree=2, commit=1, tag=8)
104 class PackWriter:
105     def __init__(self):
106         self.count = 0
107         self.binlist = []
108         self.objcache = MultiPackIndex(repodir('objects/pack'))
109         self.filename = None
110         self.file = None
111
112     def __del__(self):
113         self.close()
114
115     def _open(self):
116         assert(not self.file)
117         self.objcache.zap_also()
118         self.filename = repodir('objects/bup%d' % os.getpid())
119         self.file = open(self.filename + '.pack', 'w+')
120         self.file.write('PACK\0\0\0\2\0\0\0\0')
121
122     def _write(self, bin, type, content):
123         if not self.file:
124             self._open()
125         f = self.file
126
127         if verbose:
128             log('>')
129             
130         sz = len(content)
131         szbits = (sz & 0x0f) | (_typemap[type]<<4)
132         sz >>= 4
133         while 1:
134             if sz: szbits |= 0x80
135             f.write(chr(szbits))
136             if not sz:
137                 break
138             szbits = sz & 0x7f
139             sz >>= 7
140         
141         z = zlib.compressobj(1)
142         f.write(z.compress(content))
143         f.write(z.flush())
144
145         self.count += 1
146         self.binlist.append(bin)
147         return bin
148
149     def write(self, type, content):
150         return self._write(calc_hash(type, content), type, content)
151
152     def maybe_write(self, type, content):
153         bin = calc_hash(type, content)
154         if not self.objcache.exists(bin):
155             self._write(bin, type, content)
156             self.objcache.add(bin)
157         return bin
158
159     def new_blob(self, blob):
160         return self.maybe_write('blob', blob)
161
162     def new_tree(self, shalist):
163         shalist = sorted(shalist, key = _shalist_sort_key)
164         l = ['%s %s\0%s' % (mode,name,bin) 
165              for (mode,name,bin) in shalist]
166         return self.maybe_write('tree', ''.join(l))
167
168     def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
169         l = []
170         if tree: l.append('tree %s' % tree.encode('hex'))
171         if parent: l.append('parent %s' % parent)
172         if author: l.append('author %s %s' % (author, _git_date(adate)))
173         if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
174         l.append('')
175         l.append(msg)
176         return self.maybe_write('commit', '\n'.join(l))
177
178     def new_commit(self, ref, tree, msg):
179         now = time.time()
180         userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
181         oldref = ref and _read_ref(ref) or None
182         commit = self._new_commit(tree, oldref,
183                                   userline, now, userline, now,
184                                   msg)
185         if ref:
186             self.close()  # UGLY: needed so _update_ref can see the new objects
187             _update_ref(ref, commit.encode('hex'), oldref)
188         return commit
189
190     def abort(self):
191         f = self.file
192         if f:
193             self.file = None
194             f.close()
195             os.unlink(self.filename + '.pack')
196
197     def close(self):
198         f = self.file
199         if not f: return None
200         self.file = None
201
202         # update object count
203         f.seek(8)
204         cp = struct.pack('!i', self.count)
205         assert(len(cp) == 4)
206         f.write(cp)
207
208         # calculate the pack sha1sum
209         f.seek(0)
210         sum = sha.sha()
211         while 1:
212             b = f.read(65536)
213             sum.update(b)
214             if not b: break
215         f.write(sum.digest())
216         
217         f.close()
218
219         p = subprocess.Popen(['git', 'index-pack', '-v',
220                               self.filename + '.pack'],
221                              preexec_fn = _gitenv,
222                              stdout = subprocess.PIPE)
223         out = p.stdout.read().strip()
224         if p.wait() or not out:
225             raise Exception('git index-pack returned an error')
226         nameprefix = repodir('objects/pack/%s' % out)
227         os.rename(self.filename + '.pack', nameprefix + '.pack')
228         os.rename(self.filename + '.idx', nameprefix + '.idx')
229         return nameprefix
230
231
232 def _git_date(date):
233     return time.strftime('%s %z', time.localtime(date))
234
235
236 def _gitenv():
237     os.environ['GIT_DIR'] = os.path.abspath(repodir())
238
239
240 def _read_ref(refname):
241     p = subprocess.Popen(['git', 'show-ref', '--', refname],
242                          preexec_fn = _gitenv,
243                          stdout = subprocess.PIPE)
244     out = p.stdout.read().strip()
245     p.wait()
246     if out:
247         return out.split()[0]
248     else:
249         return None
250
251
252 def _update_ref(refname, newval, oldval):
253     if not oldval:
254         oldval = ''
255     p = subprocess.Popen(['git', 'update-ref', '--', refname, newval, oldval],
256                          preexec_fn = _gitenv)
257     p.wait()
258     return newval
259
260
261 def init_repo():
262     d = repodir()
263     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
264         raise Exception('"%d" exists but is not a directory\n' % d)
265     p = subprocess.Popen(['git', 'init', '--bare'],
266                          preexec_fn = _gitenv)
267     return p.wait()