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