]> arthur.barton.de Git - bup.git/blob - lib/bup/git.py
git: remove global variable ignore_midx
[bup.git] / lib / bup / git.py
1 """Git interaction library.
2 bup repositories are in Git format. This library allows us to
3 interact with the Git data structures.
4 """
5
6 from __future__ import absolute_import
7 import errno, os, sys, zlib, time, subprocess, struct, stat, re, tempfile, glob
8 from collections import namedtuple
9 from itertools import islice
10 from numbers import Integral
11
12 from bup import _helpers, compat, hashsplit, path, midx, bloom, xstat
13 from bup.compat import range
14 from bup.helpers import (Sha1, add_error, chunkyreader, debug1, debug2,
15                          fdatasync,
16                          hostname, localtime, log,
17                          merge_dict,
18                          merge_iter,
19                          mmap_read, mmap_readwrite,
20                          parse_num,
21                          progress, qprogress, shstr, stat_if_exists,
22                          unlink,
23                          utc_offset_str)
24 from bup.pwdgrp import username, userfullname
25
26 verbose = 0
27 repodir = None  # The default repository, once initialized
28
29 _typemap =  { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
30 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
31
32 _total_searches = 0
33 _total_steps = 0
34
35
36 class GitError(Exception):
37     pass
38
39
40 def _gitenv(repo_dir=None):
41     if not repo_dir:
42         repo_dir = repo()
43     return merge_dict(os.environ, {'GIT_DIR': os.path.abspath(repo_dir)})
44
45 def _git_wait(cmd, p):
46     rv = p.wait()
47     if rv != 0:
48         raise GitError('%s returned %d' % (shstr(cmd), rv))
49
50 def _git_capture(argv):
51     p = subprocess.Popen(argv, stdout=subprocess.PIPE, env=_gitenv())
52     r = p.stdout.read()
53     _git_wait(repr(argv), p)
54     return r
55
56 def git_config_get(option, repo_dir=None):
57     cmd = ('git', 'config', '--get', option)
58     p = subprocess.Popen(cmd, stdout=subprocess.PIPE,
59                          env=_gitenv(repo_dir=repo_dir))
60     r = p.stdout.read()
61     rc = p.wait()
62     if rc == 0:
63         return r
64     if rc != 1:
65         raise GitError('%s returned %d' % (cmd, rc))
66     return None
67
68
69 def parse_tz_offset(s):
70     """UTC offset in seconds."""
71     tz_off = (int(s[1:3]) * 60 * 60) + (int(s[3:5]) * 60)
72     if s[0] == '-':
73         return - tz_off
74     return tz_off
75
76
77 # FIXME: derived from http://git.rsbx.net/Documents/Git_Data_Formats.txt
78 # Make sure that's authoritative.
79 _start_end_char = r'[^ .,:;<>"\'\0\n]'
80 _content_char = r'[^\0\n<>]'
81 _safe_str_rx = '(?:%s{1,2}|(?:%s%s*%s))' \
82     % (_start_end_char,
83        _start_end_char, _content_char, _start_end_char)
84 _tz_rx = r'[-+]\d\d[0-5]\d'
85 _parent_rx = r'(?:parent [abcdefABCDEF0123456789]{40}\n)'
86 # Assumes every following line starting with a space is part of the
87 # mergetag.  Is there a formal commit blob spec?
88 _mergetag_rx = r'(?:\nmergetag object [abcdefABCDEF0123456789]{40}(?:\n [^\0\n]*)*)'
89 _commit_rx = re.compile(r'''tree (?P<tree>[abcdefABCDEF0123456789]{40})
90 (?P<parents>%s*)author (?P<author_name>%s) <(?P<author_mail>%s)> (?P<asec>\d+) (?P<atz>%s)
91 committer (?P<committer_name>%s) <(?P<committer_mail>%s)> (?P<csec>\d+) (?P<ctz>%s)(?P<mergetag>%s?)
92
93 (?P<message>(?:.|\n)*)''' % (_parent_rx,
94                              _safe_str_rx, _safe_str_rx, _tz_rx,
95                              _safe_str_rx, _safe_str_rx, _tz_rx,
96                              _mergetag_rx))
97 _parent_hash_rx = re.compile(r'\s*parent ([abcdefABCDEF0123456789]{40})\s*')
98
99 # Note that the author_sec and committer_sec values are (UTC) epoch
100 # seconds, and for now the mergetag is not included.
101 CommitInfo = namedtuple('CommitInfo', ['tree', 'parents',
102                                        'author_name', 'author_mail',
103                                        'author_sec', 'author_offset',
104                                        'committer_name', 'committer_mail',
105                                        'committer_sec', 'committer_offset',
106                                        'message'])
107
108 def parse_commit(content):
109     commit_match = re.match(_commit_rx, content)
110     if not commit_match:
111         raise Exception('cannot parse commit %r' % content)
112     matches = commit_match.groupdict()
113     return CommitInfo(tree=matches['tree'],
114                       parents=re.findall(_parent_hash_rx, matches['parents']),
115                       author_name=matches['author_name'],
116                       author_mail=matches['author_mail'],
117                       author_sec=int(matches['asec']),
118                       author_offset=parse_tz_offset(matches['atz']),
119                       committer_name=matches['committer_name'],
120                       committer_mail=matches['committer_mail'],
121                       committer_sec=int(matches['csec']),
122                       committer_offset=parse_tz_offset(matches['ctz']),
123                       message=matches['message'])
124
125
126 def get_cat_data(cat_iterator, expected_type):
127     _, kind, _ = next(cat_iterator)
128     if kind != expected_type:
129         raise Exception('expected %r, saw %r' % (expected_type, kind))
130     return ''.join(cat_iterator)
131
132 def get_commit_items(id, cp):
133     return parse_commit(get_cat_data(cp.get(id), 'commit'))
134
135 def _local_git_date_str(epoch_sec):
136     return '%d %s' % (epoch_sec, utc_offset_str(epoch_sec))
137
138
139 def _git_date_str(epoch_sec, tz_offset_sec):
140     offs =  tz_offset_sec // 60
141     return '%d %s%02d%02d' \
142         % (epoch_sec,
143            '+' if offs >= 0 else '-',
144            abs(offs) // 60,
145            abs(offs) % 60)
146
147
148 def repo(sub = '', repo_dir=None):
149     """Get the path to the git repository or one of its subdirectories."""
150     repo_dir = repo_dir or repodir
151     if not repo_dir:
152         raise GitError('You should call check_repo_or_die()')
153
154     # If there's a .git subdirectory, then the actual repo is in there.
155     gd = os.path.join(repo_dir, '.git')
156     if os.path.exists(gd):
157         repo_dir = gd
158
159     return os.path.join(repo_dir, sub)
160
161
162 def shorten_hash(s):
163     return re.sub(r'([^0-9a-z]|\b)([0-9a-z]{7})[0-9a-z]{33}([^0-9a-z]|\b)',
164                   r'\1\2*\3', s)
165
166
167 def repo_rel(path):
168     full = os.path.abspath(path)
169     fullrepo = os.path.abspath(repo(''))
170     if not fullrepo.endswith('/'):
171         fullrepo += '/'
172     if full.startswith(fullrepo):
173         path = full[len(fullrepo):]
174     if path.startswith('index-cache/'):
175         path = path[len('index-cache/'):]
176     return shorten_hash(path)
177
178
179 def all_packdirs():
180     paths = [repo('objects/pack')]
181     paths += glob.glob(repo('index-cache/*/.'))
182     return paths
183
184
185 def auto_midx(objdir):
186     args = [path.exe(), 'midx', '--auto', '--dir', objdir]
187     try:
188         rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
189     except OSError as e:
190         # make sure 'args' gets printed to help with debugging
191         add_error('%r: exception: %s' % (args, e))
192         raise
193     if rv:
194         add_error('%r: returned %d' % (args, rv))
195
196     args = [path.exe(), 'bloom', '--dir', objdir]
197     try:
198         rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
199     except OSError as e:
200         # make sure 'args' gets printed to help with debugging
201         add_error('%r: exception: %s' % (args, e))
202         raise
203     if rv:
204         add_error('%r: returned %d' % (args, rv))
205
206
207 def mangle_name(name, mode, gitmode):
208     """Mangle a file name to present an abstract name for segmented files.
209     Mangled file names will have the ".bup" extension added to them. If a
210     file's name already ends with ".bup", a ".bupl" extension is added to
211     disambiguate normal files from segmented ones.
212     """
213     if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
214         assert(stat.S_ISDIR(gitmode))
215         return name + '.bup'
216     elif name.endswith('.bup') or name[:-1].endswith('.bup'):
217         return name + '.bupl'
218     else:
219         return name
220
221
222 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
223 def demangle_name(name, mode):
224     """Remove name mangling from a file name, if necessary.
225
226     The return value is a tuple (demangled_filename,mode), where mode is one of
227     the following:
228
229     * BUP_NORMAL  : files that should be read as-is from the repository
230     * BUP_CHUNKED : files that were chunked and need to be reassembled
231
232     For more information on the name mangling algorithm, see mangle_name()
233     """
234     if name.endswith('.bupl'):
235         return (name[:-5], BUP_NORMAL)
236     elif name.endswith('.bup'):
237         return (name[:-4], BUP_CHUNKED)
238     elif name.endswith('.bupm'):
239         return (name[:-5],
240                 BUP_CHUNKED if stat.S_ISDIR(mode) else BUP_NORMAL)
241     else:
242         return (name, BUP_NORMAL)
243
244
245 def calc_hash(type, content):
246     """Calculate some content's hash in the Git fashion."""
247     header = '%s %d\0' % (type, len(content))
248     sum = Sha1(header)
249     sum.update(content)
250     return sum.digest()
251
252
253 def shalist_item_sort_key(ent):
254     (mode, name, id) = ent
255     assert(mode+0 == mode)
256     if stat.S_ISDIR(mode):
257         return name + '/'
258     else:
259         return name
260
261
262 def tree_encode(shalist):
263     """Generate a git tree object from (mode,name,hash) tuples."""
264     shalist = sorted(shalist, key = shalist_item_sort_key)
265     l = []
266     for (mode,name,bin) in shalist:
267         assert(mode)
268         assert(mode+0 == mode)
269         assert(name)
270         assert(len(bin) == 20)
271         s = '%o %s\0%s' % (mode,name,bin)
272         assert(s[0] != '0')  # 0-padded octal is not acceptable in a git tree
273         l.append(s)
274     return ''.join(l)
275
276
277 def tree_decode(buf):
278     """Generate a list of (mode,name,hash) from the git tree object in buf."""
279     ofs = 0
280     while ofs < len(buf):
281         z = buf.find('\0', ofs)
282         assert(z > ofs)
283         spl = buf[ofs:z].split(' ', 1)
284         assert(len(spl) == 2)
285         mode,name = spl
286         sha = buf[z+1:z+1+20]
287         ofs = z+1+20
288         yield (int(mode, 8), name, sha)
289
290
291 def _encode_packobj(type, content, compression_level=1):
292     if compression_level not in (0, 1, 2, 3, 4, 5, 6, 7, 8, 9):
293         raise ValueError('invalid compression level %s' % compression_level)
294     szout = ''
295     sz = len(content)
296     szbits = (sz & 0x0f) | (_typemap[type]<<4)
297     sz >>= 4
298     while 1:
299         if sz: szbits |= 0x80
300         szout += chr(szbits)
301         if not sz:
302             break
303         szbits = sz & 0x7f
304         sz >>= 7
305     z = zlib.compressobj(compression_level)
306     yield szout
307     yield z.compress(content)
308     yield z.flush()
309
310
311 def _encode_looseobj(type, content, compression_level=1):
312     z = zlib.compressobj(compression_level)
313     yield z.compress('%s %d\0' % (type, len(content)))
314     yield z.compress(content)
315     yield z.flush()
316
317
318 def _decode_looseobj(buf):
319     assert(buf);
320     s = zlib.decompress(buf)
321     i = s.find('\0')
322     assert(i > 0)
323     l = s[:i].split(' ')
324     type = l[0]
325     sz = int(l[1])
326     content = s[i+1:]
327     assert(type in _typemap)
328     assert(sz == len(content))
329     return (type, content)
330
331
332 def _decode_packobj(buf):
333     assert(buf)
334     c = ord(buf[0])
335     type = _typermap[(c & 0x70) >> 4]
336     sz = c & 0x0f
337     shift = 4
338     i = 0
339     while c & 0x80:
340         i += 1
341         c = ord(buf[i])
342         sz |= (c & 0x7f) << shift
343         shift += 7
344         if not (c & 0x80):
345             break
346     return (type, zlib.decompress(buf[i+1:]))
347
348
349 class PackIdx:
350     def __init__(self):
351         assert(0)
352
353     def find_offset(self, hash):
354         """Get the offset of an object inside the index file."""
355         idx = self._idx_from_hash(hash)
356         if idx != None:
357             return self._ofs_from_idx(idx)
358         return None
359
360     def exists(self, hash, want_source=False):
361         """Return nonempty if the object exists in this index."""
362         if hash and (self._idx_from_hash(hash) != None):
363             return want_source and os.path.basename(self.name) or True
364         return None
365
366     def __len__(self):
367         return int(self.fanout[255])
368
369     def _idx_from_hash(self, hash):
370         global _total_searches, _total_steps
371         _total_searches += 1
372         assert(len(hash) == 20)
373         b1 = ord(hash[0])
374         start = self.fanout[b1-1] # range -1..254
375         end = self.fanout[b1] # range 0..255
376         want = str(hash)
377         _total_steps += 1  # lookup table is a step
378         while start < end:
379             _total_steps += 1
380             mid = start + (end-start)/2
381             v = self._idx_to_hash(mid)
382             if v < want:
383                 start = mid+1
384             elif v > want:
385                 end = mid
386             else: # got it!
387                 return mid
388         return None
389
390
391 class PackIdxV1(PackIdx):
392     """Object representation of a Git pack index (version 1) file."""
393     def __init__(self, filename, f):
394         self.name = filename
395         self.idxnames = [self.name]
396         self.map = mmap_read(f)
397         self.fanout = list(struct.unpack('!256I', buffer(self.map, 0, 256 * 4)))
398         self.fanout.append(0)  # entry "-1"
399         nsha = self.fanout[255]
400         self.sha_ofs = 256*4
401         self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
402
403     def _ofs_from_idx(self, idx):
404         ofs = idx * 24
405         return struct.unpack('!I', self.shatable[ofs : ofs + 4])[0]
406
407     def _idx_to_hash(self, idx):
408         ofs = idx * 24 + 4
409         return self.shatable[ofs : ofs + 20]
410
411     def __iter__(self):
412         count = self.fanout[255]
413         start = 256 * 4 + 4
414         for ofs in range(start, start + (24 * count), 24):
415             yield self.map[ofs : ofs + 20]
416
417
418 class PackIdxV2(PackIdx):
419     """Object representation of a Git pack index (version 2) file."""
420     def __init__(self, filename, f):
421         self.name = filename
422         self.idxnames = [self.name]
423         self.map = mmap_read(f)
424         assert self.map[0:8] == b'\377tOc\0\0\0\2'
425         self.fanout = list(struct.unpack('!256I',
426                                          buffer(self.map[8 : 8 + 256 * 4])))
427         self.fanout.append(0)  # entry "-1"
428         nsha = self.fanout[255]
429         self.sha_ofs = 8 + 256*4
430         self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
431         self.ofstable = buffer(self.map,
432                                self.sha_ofs + nsha*20 + nsha*4,
433                                nsha*4)
434         self.ofs64table = buffer(self.map,
435                                  8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
436
437     def _ofs_from_idx(self, idx):
438         i = idx * 4
439         ofs = struct.unpack('!I', self.ofstable[i : i + 4])[0]
440         if ofs & 0x80000000:
441             idx64 = ofs & 0x7fffffff
442             idx64_i = idx64 * 8
443             ofs = struct.unpack('!Q', self.ofs64table[idx64_i : idx64_i + 8])[0]
444         return ofs
445
446     def _idx_to_hash(self, idx):
447         return self.shatable[idx * 20 : (idx + 1) * 20]
448
449     def __iter__(self):
450         count = self.fanout[255]
451         start = 8 + 256 * 4
452         for ofs in range(start, start + (20 * count), 20):
453             yield self.map[ofs : ofs + 20]
454
455
456 _mpi_count = 0
457 class PackIdxList:
458     def __init__(self, dir, ignore_midx=False):
459         global _mpi_count
460         assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
461         _mpi_count += 1
462         self.dir = dir
463         self.also = set()
464         self.packs = []
465         self.do_bloom = False
466         self.bloom = None
467         self.ignore_midx = ignore_midx
468         self.refresh()
469
470     def __del__(self):
471         global _mpi_count
472         _mpi_count -= 1
473         assert(_mpi_count == 0)
474
475     def __iter__(self):
476         return iter(idxmerge(self.packs))
477
478     def __len__(self):
479         return sum(len(pack) for pack in self.packs)
480
481     def exists(self, hash, want_source=False):
482         """Return nonempty if the object exists in the index files."""
483         global _total_searches
484         _total_searches += 1
485         if hash in self.also:
486             return True
487         if self.do_bloom and self.bloom:
488             if self.bloom.exists(hash):
489                 self.do_bloom = False
490             else:
491                 _total_searches -= 1  # was counted by bloom
492                 return None
493         for i in xrange(len(self.packs)):
494             p = self.packs[i]
495             _total_searches -= 1  # will be incremented by sub-pack
496             ix = p.exists(hash, want_source=want_source)
497             if ix:
498                 # reorder so most recently used packs are searched first
499                 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
500                 return ix
501         self.do_bloom = True
502         return None
503
504     def refresh(self, skip_midx = False):
505         """Refresh the index list.
506         This method verifies if .midx files were superseded (e.g. all of its
507         contents are in another, bigger .midx file) and removes the superseded
508         files.
509
510         If skip_midx is True, all work on .midx files will be skipped and .midx
511         files will be removed from the list.
512
513         The instance variable 'ignore_midx' can force this function to
514         always act as if skip_midx was True.
515         """
516         self.bloom = None # Always reopen the bloom as it may have been relaced
517         self.do_bloom = False
518         skip_midx = skip_midx or self.ignore_midx
519         d = dict((p.name, p) for p in self.packs
520                  if not skip_midx or not isinstance(p, midx.PackMidx))
521         if os.path.exists(self.dir):
522             if not skip_midx:
523                 midxl = []
524                 for ix in self.packs:
525                     if isinstance(ix, midx.PackMidx):
526                         for name in ix.idxnames:
527                             d[os.path.join(self.dir, name)] = ix
528                 for full in glob.glob(os.path.join(self.dir,'*.midx')):
529                     if not d.get(full):
530                         mx = midx.PackMidx(full)
531                         (mxd, mxf) = os.path.split(mx.name)
532                         broken = False
533                         for n in mx.idxnames:
534                             if not os.path.exists(os.path.join(mxd, n)):
535                                 log(('warning: index %s missing\n' +
536                                     '  used by %s\n') % (n, mxf))
537                                 broken = True
538                         if broken:
539                             mx.close()
540                             del mx
541                             unlink(full)
542                         else:
543                             midxl.append(mx)
544                 midxl.sort(key=lambda ix:
545                            (-len(ix), -xstat.stat(ix.name).st_mtime))
546                 for ix in midxl:
547                     any_needed = False
548                     for sub in ix.idxnames:
549                         found = d.get(os.path.join(self.dir, sub))
550                         if not found or isinstance(found, PackIdx):
551                             # doesn't exist, or exists but not in a midx
552                             any_needed = True
553                             break
554                     if any_needed:
555                         d[ix.name] = ix
556                         for name in ix.idxnames:
557                             d[os.path.join(self.dir, name)] = ix
558                     elif not ix.force_keep:
559                         debug1('midx: removing redundant: %s\n'
560                                % os.path.basename(ix.name))
561                         ix.close()
562                         unlink(ix.name)
563             for full in glob.glob(os.path.join(self.dir,'*.idx')):
564                 if not d.get(full):
565                     try:
566                         ix = open_idx(full)
567                     except GitError as e:
568                         add_error(e)
569                         continue
570                     d[full] = ix
571             bfull = os.path.join(self.dir, 'bup.bloom')
572             if self.bloom is None and os.path.exists(bfull):
573                 self.bloom = bloom.ShaBloom(bfull)
574             self.packs = list(set(d.values()))
575             self.packs.sort(reverse=True, key=lambda x: len(x))
576             if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
577                 self.do_bloom = True
578             else:
579                 self.bloom = None
580         debug1('PackIdxList: using %d index%s.\n'
581             % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
582
583     def add(self, hash):
584         """Insert an additional object in the list."""
585         self.also.add(hash)
586
587
588 def open_idx(filename):
589     if filename.endswith('.idx'):
590         f = open(filename, 'rb')
591         header = f.read(8)
592         if header[0:4] == '\377tOc':
593             version = struct.unpack('!I', header[4:8])[0]
594             if version == 2:
595                 return PackIdxV2(filename, f)
596             else:
597                 raise GitError('%s: expected idx file version 2, got %d'
598                                % (filename, version))
599         elif len(header) == 8 and header[0:4] < '\377tOc':
600             return PackIdxV1(filename, f)
601         else:
602             raise GitError('%s: unrecognized idx file header' % filename)
603     elif filename.endswith('.midx'):
604         return midx.PackMidx(filename)
605     else:
606         raise GitError('idx filenames must end with .idx or .midx')
607
608
609 def idxmerge(idxlist, final_progress=True):
610     """Generate a list of all the objects reachable in a PackIdxList."""
611     def pfunc(count, total):
612         qprogress('Reading indexes: %.2f%% (%d/%d)\r'
613                   % (count*100.0/total, count, total))
614     def pfinal(count, total):
615         if final_progress:
616             progress('Reading indexes: %.2f%% (%d/%d), done.\n'
617                      % (100, total, total))
618     return merge_iter(idxlist, 10024, pfunc, pfinal)
619
620
621 def _make_objcache():
622     return PackIdxList(repo('objects/pack'))
623
624 # bup-gc assumes that it can disable all PackWriter activities
625 # (bloom/midx/cache) via the constructor and close() arguments.
626
627 class PackWriter:
628     """Writes Git objects inside a pack file."""
629     def __init__(self, objcache_maker=_make_objcache, compression_level=1,
630                  run_midx=True, on_pack_finish=None,
631                  max_pack_size=None, max_pack_objects=None, repo_dir=None):
632         self.repo_dir = repo_dir or repo()
633         self.file = None
634         self.parentfd = None
635         self.count = 0
636         self.outbytes = 0
637         self.filename = None
638         self.idx = None
639         self.objcache_maker = objcache_maker
640         self.objcache = None
641         self.compression_level = compression_level
642         self.run_midx=run_midx
643         self.on_pack_finish = on_pack_finish
644         if not max_pack_size:
645             max_pack_size = git_config_get('pack.packSizeLimit',
646                                            repo_dir=self.repo_dir)
647             if max_pack_size is not None:
648                 max_pack_size = parse_num(max_pack_size)
649             if not max_pack_size:
650                 # larger packs slow down pruning
651                 max_pack_size = 1000 * 1000 * 1000
652         self.max_pack_size = max_pack_size
653         # cache memory usage is about 83 bytes per object
654         self.max_pack_objects = max_pack_objects if max_pack_objects \
655                                 else max(1, self.max_pack_size // 5000)
656
657     def __del__(self):
658         self.close()
659
660     def __enter__(self):
661         return self
662
663     def __exit__(self, type, value, traceback):
664         self.close()
665
666     def _open(self):
667         if not self.file:
668             objdir = dir = os.path.join(self.repo_dir, 'objects')
669             fd, name = tempfile.mkstemp(suffix='.pack', dir=objdir)
670             try:
671                 self.file = os.fdopen(fd, 'w+b')
672             except:
673                 os.close(fd)
674                 raise
675             try:
676                 self.parentfd = os.open(objdir, os.O_RDONLY)
677             except:
678                 f = self.file
679                 self.file = None
680                 f.close()
681                 raise
682             assert(name.endswith('.pack'))
683             self.filename = name[:-5]
684             self.file.write('PACK\0\0\0\2\0\0\0\0')
685             self.idx = list(list() for i in xrange(256))
686
687     def _raw_write(self, datalist, sha):
688         self._open()
689         f = self.file
690         # in case we get interrupted (eg. KeyboardInterrupt), it's best if
691         # the file never has a *partial* blob.  So let's make sure it's
692         # all-or-nothing.  (The blob shouldn't be very big anyway, thanks
693         # to our hashsplit algorithm.)  f.write() does its own buffering,
694         # but that's okay because we'll flush it in _end().
695         oneblob = ''.join(datalist)
696         try:
697             f.write(oneblob)
698         except IOError as e:
699             raise GitError, e, sys.exc_info()[2]
700         nw = len(oneblob)
701         crc = zlib.crc32(oneblob) & 0xffffffff
702         self._update_idx(sha, crc, nw)
703         self.outbytes += nw
704         self.count += 1
705         return nw, crc
706
707     def _update_idx(self, sha, crc, size):
708         assert(sha)
709         if self.idx:
710             self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
711
712     def _write(self, sha, type, content):
713         if verbose:
714             log('>')
715         if not sha:
716             sha = calc_hash(type, content)
717         size, crc = self._raw_write(_encode_packobj(type, content,
718                                                     self.compression_level),
719                                     sha=sha)
720         if self.outbytes >= self.max_pack_size \
721            or self.count >= self.max_pack_objects:
722             self.breakpoint()
723         return sha
724
725     def breakpoint(self):
726         """Clear byte and object counts and return the last processed id."""
727         id = self._end(self.run_midx)
728         self.outbytes = self.count = 0
729         return id
730
731     def _require_objcache(self):
732         if self.objcache is None and self.objcache_maker:
733             self.objcache = self.objcache_maker()
734         if self.objcache is None:
735             raise GitError(
736                     "PackWriter not opened or can't check exists w/o objcache")
737
738     def exists(self, id, want_source=False):
739         """Return non-empty if an object is found in the object cache."""
740         self._require_objcache()
741         return self.objcache.exists(id, want_source=want_source)
742
743     def just_write(self, sha, type, content):
744         """Write an object to the pack file without checking for duplication."""
745         self._write(sha, type, content)
746         # If nothing else, gc doesn't have/want an objcache
747         if self.objcache is not None:
748             self.objcache.add(sha)
749
750     def maybe_write(self, type, content):
751         """Write an object to the pack file if not present and return its id."""
752         sha = calc_hash(type, content)
753         if not self.exists(sha):
754             self._require_objcache()
755             self.just_write(sha, type, content)
756         return sha
757
758     def new_blob(self, blob):
759         """Create a blob object in the pack with the supplied content."""
760         return self.maybe_write('blob', blob)
761
762     def new_tree(self, shalist):
763         """Create a tree object in the pack."""
764         content = tree_encode(shalist)
765         return self.maybe_write('tree', content)
766
767     def new_commit(self, tree, parent,
768                    author, adate_sec, adate_tz,
769                    committer, cdate_sec, cdate_tz,
770                    msg):
771         """Create a commit object in the pack.  The date_sec values must be
772         epoch-seconds, and if a tz is None, the local timezone is assumed."""
773         if adate_tz:
774             adate_str = _git_date_str(adate_sec, adate_tz)
775         else:
776             adate_str = _local_git_date_str(adate_sec)
777         if cdate_tz:
778             cdate_str = _git_date_str(cdate_sec, cdate_tz)
779         else:
780             cdate_str = _local_git_date_str(cdate_sec)
781         l = []
782         if tree: l.append('tree %s' % tree.encode('hex'))
783         if parent: l.append('parent %s' % parent.encode('hex'))
784         if author: l.append('author %s %s' % (author, adate_str))
785         if committer: l.append('committer %s %s' % (committer, cdate_str))
786         l.append('')
787         l.append(msg)
788         return self.maybe_write('commit', '\n'.join(l))
789
790     def abort(self):
791         """Remove the pack file from disk."""
792         f = self.file
793         if f:
794             pfd = self.parentfd
795             self.file = None
796             self.parentfd = None
797             self.idx = None
798             try:
799                 try:
800                     os.unlink(self.filename + '.pack')
801                 finally:
802                     f.close()
803             finally:
804                 if pfd is not None:
805                     os.close(pfd)
806
807     def _end(self, run_midx=True):
808         f = self.file
809         if not f: return None
810         self.file = None
811         try:
812             self.objcache = None
813             idx = self.idx
814             self.idx = None
815
816             # update object count
817             f.seek(8)
818             cp = struct.pack('!i', self.count)
819             assert(len(cp) == 4)
820             f.write(cp)
821
822             # calculate the pack sha1sum
823             f.seek(0)
824             sum = Sha1()
825             for b in chunkyreader(f):
826                 sum.update(b)
827             packbin = sum.digest()
828             f.write(packbin)
829             fdatasync(f.fileno())
830         finally:
831             f.close()
832
833         obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
834         nameprefix = os.path.join(self.repo_dir,
835                                   'objects/pack/pack-' +  obj_list_sha)
836         if os.path.exists(self.filename + '.map'):
837             os.unlink(self.filename + '.map')
838         os.rename(self.filename + '.pack', nameprefix + '.pack')
839         os.rename(self.filename + '.idx', nameprefix + '.idx')
840         try:
841             os.fsync(self.parentfd)
842         finally:
843             os.close(self.parentfd)
844
845         if run_midx:
846             auto_midx(os.path.join(self.repo_dir, 'objects/pack'))
847
848         if self.on_pack_finish:
849             self.on_pack_finish(nameprefix)
850
851         return nameprefix
852
853     def close(self, run_midx=True):
854         """Close the pack file and move it to its definitive path."""
855         return self._end(run_midx=run_midx)
856
857     def _write_pack_idx_v2(self, filename, idx, packbin):
858         ofs64_count = 0
859         for section in idx:
860             for entry in section:
861                 if entry[2] >= 2**31:
862                     ofs64_count += 1
863
864         # Length: header + fan-out + shas-and-crcs + overflow-offsets
865         index_len = 8 + (4 * 256) + (28 * self.count) + (8 * ofs64_count)
866         idx_map = None
867         idx_f = open(filename, 'w+b')
868         try:
869             idx_f.truncate(index_len)
870             fdatasync(idx_f.fileno())
871             idx_map = mmap_readwrite(idx_f, close=False)
872             try:
873                 count = _helpers.write_idx(filename, idx_map, idx, self.count)
874                 assert(count == self.count)
875                 idx_map.flush()
876             finally:
877                 idx_map.close()
878         finally:
879             idx_f.close()
880
881         idx_f = open(filename, 'a+b')
882         try:
883             idx_f.write(packbin)
884             idx_f.seek(0)
885             idx_sum = Sha1()
886             b = idx_f.read(8 + 4*256)
887             idx_sum.update(b)
888
889             obj_list_sum = Sha1()
890             for b in chunkyreader(idx_f, 20*self.count):
891                 idx_sum.update(b)
892                 obj_list_sum.update(b)
893             namebase = obj_list_sum.hexdigest()
894
895             for b in chunkyreader(idx_f):
896                 idx_sum.update(b)
897             idx_f.write(idx_sum.digest())
898             fdatasync(idx_f.fileno())
899             return namebase
900         finally:
901             idx_f.close()
902
903
904 def list_refs(patterns=None, repo_dir=None,
905               limit_to_heads=False, limit_to_tags=False):
906     """Yield (refname, hash) tuples for all repository refs unless
907     patterns are specified.  In that case, only include tuples for
908     refs matching those patterns (cf. git-show-ref(1)).  The limits
909     restrict the result items to refs/heads or refs/tags.  If both
910     limits are specified, items from both sources will be included.
911
912     """
913     argv = ['git', 'show-ref']
914     if limit_to_heads:
915         argv.append('--heads')
916     if limit_to_tags:
917         argv.append('--tags')
918     argv.append('--')
919     if patterns:
920         argv.extend(patterns)
921     p = subprocess.Popen(argv, env=_gitenv(repo_dir), stdout=subprocess.PIPE)
922     out = p.stdout.read().strip()
923     rv = p.wait()  # not fatal
924     if rv:
925         assert(not out)
926     if out:
927         for d in out.split('\n'):
928             (sha, name) = d.split(' ', 1)
929             yield (name, sha.decode('hex'))
930
931
932 def read_ref(refname, repo_dir = None):
933     """Get the commit id of the most recent commit made on a given ref."""
934     refs = list_refs(patterns=[refname], repo_dir=repo_dir, limit_to_heads=True)
935     l = tuple(islice(refs, 2))
936     if l:
937         assert(len(l) == 1)
938         return l[0][1]
939     else:
940         return None
941
942
943 def rev_list_invocation(ref_or_refs, count=None, format=None):
944     if isinstance(ref_or_refs, compat.str_type):
945         refs = (ref_or_refs,)
946     else:
947         refs = ref_or_refs
948     argv = ['git', 'rev-list']
949     if isinstance(count, Integral):
950         argv.extend(['-n', str(count)])
951     elif count:
952         raise ValueError('unexpected count argument %r' % count)
953
954     if format:
955         argv.append('--pretty=format:' + format)
956     for ref in refs:
957         assert not ref.startswith('-')
958         argv.append(ref)
959     argv.append('--')
960     return argv
961
962
963 def rev_list(ref_or_refs, count=None, parse=None, format=None, repo_dir=None):
964     """Yield information about commits as per "git rev-list".  If a format
965     is not provided, yield one hex hash at a time.  If a format is
966     provided, pass it to rev-list and call parse(git_stdout) for each
967     commit with the stream positioned just after the rev-list "commit
968     HASH" header line.  When a format is provided yield (oidx,
969     parse(git_stdout)) for each commit.
970
971     """
972     assert bool(parse) == bool(format)
973     p = subprocess.Popen(rev_list_invocation(ref_or_refs, count=count,
974                                              format=format),
975                          env=_gitenv(repo_dir),
976                          stdout = subprocess.PIPE)
977     if not format:
978         for line in p.stdout:
979             yield line.strip()
980     else:
981         line = p.stdout.readline()
982         while line:
983             s = line.strip()
984             if not s.startswith('commit '):
985                 raise Exception('unexpected line ' + s)
986             s = s[7:]
987             assert len(s) == 40
988             yield s, parse(p.stdout)
989             line = p.stdout.readline()
990
991     rv = p.wait()  # not fatal
992     if rv:
993         raise GitError, 'git rev-list returned error %d' % rv
994
995
996 def get_commit_dates(refs, repo_dir=None):
997     """Get the dates for the specified commit refs.  For now, every unique
998        string in refs must resolve to a different commit or this
999        function will fail."""
1000     result = []
1001     for ref in refs:
1002         commit = get_commit_items(ref, cp(repo_dir))
1003         result.append(commit.author_sec)
1004     return result
1005
1006
1007 def rev_parse(committish, repo_dir=None):
1008     """Resolve the full hash for 'committish', if it exists.
1009
1010     Should be roughly equivalent to 'git rev-parse'.
1011
1012     Returns the hex value of the hash if it is found, None if 'committish' does
1013     not correspond to anything.
1014     """
1015     head = read_ref(committish, repo_dir=repo_dir)
1016     if head:
1017         debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
1018         return head
1019
1020     pL = PackIdxList(repo('objects/pack', repo_dir=repo_dir))
1021
1022     if len(committish) == 40:
1023         try:
1024             hash = committish.decode('hex')
1025         except TypeError:
1026             return None
1027
1028         if pL.exists(hash):
1029             return hash
1030
1031     return None
1032
1033
1034 def update_ref(refname, newval, oldval, repo_dir=None):
1035     """Update a repository reference."""
1036     if not oldval:
1037         oldval = ''
1038     assert(refname.startswith('refs/heads/') \
1039            or refname.startswith('refs/tags/'))
1040     p = subprocess.Popen(['git', 'update-ref', refname,
1041                           newval.encode('hex'), oldval.encode('hex')],
1042                          env=_gitenv(repo_dir))
1043     _git_wait('git update-ref', p)
1044
1045
1046 def delete_ref(refname, oldvalue=None):
1047     """Delete a repository reference (see git update-ref(1))."""
1048     assert(refname.startswith('refs/'))
1049     oldvalue = [] if not oldvalue else [oldvalue]
1050     p = subprocess.Popen(['git', 'update-ref', '-d', refname] + oldvalue,
1051                          env=_gitenv())
1052     _git_wait('git update-ref', p)
1053
1054
1055 def guess_repo(path=None):
1056     """Set the path value in the global variable "repodir".
1057     This makes bup look for an existing bup repository, but not fail if a
1058     repository doesn't exist. Usually, if you are interacting with a bup
1059     repository, you would not be calling this function but using
1060     check_repo_or_die().
1061     """
1062     global repodir
1063     if path:
1064         repodir = path
1065     if not repodir:
1066         repodir = os.environ.get('BUP_DIR')
1067         if not repodir:
1068             repodir = os.path.expanduser('~/.bup')
1069
1070
1071 def init_repo(path=None):
1072     """Create the Git bare repository for bup in a given path."""
1073     guess_repo(path)
1074     d = repo()  # appends a / to the path
1075     parent = os.path.dirname(os.path.dirname(d))
1076     if parent and not os.path.exists(parent):
1077         raise GitError('parent directory "%s" does not exist\n' % parent)
1078     if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
1079         raise GitError('"%s" exists but is not a directory\n' % d)
1080     p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
1081                          env=_gitenv())
1082     _git_wait('git init', p)
1083     # Force the index version configuration in order to ensure bup works
1084     # regardless of the version of the installed Git binary.
1085     p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
1086                          stdout=sys.stderr, env=_gitenv())
1087     _git_wait('git config', p)
1088     # Enable the reflog
1089     p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
1090                          stdout=sys.stderr, env=_gitenv())
1091     _git_wait('git config', p)
1092
1093
1094 def check_repo_or_die(path=None):
1095     """Check to see if a bup repository probably exists, and abort if not."""
1096     guess_repo(path)
1097     top = repo()
1098     pst = stat_if_exists(top + '/objects/pack')
1099     if pst and stat.S_ISDIR(pst.st_mode):
1100         return
1101     if not pst:
1102         top_st = stat_if_exists(top)
1103         if not top_st:
1104             log('error: repository %r does not exist (see "bup help init")\n'
1105                 % top)
1106             sys.exit(15)
1107     log('error: %r is not a repository\n' % top)
1108     sys.exit(14)
1109
1110
1111 _ver = None
1112 def ver():
1113     """Get Git's version and ensure a usable version is installed.
1114
1115     The returned version is formatted as an ordered tuple with each position
1116     representing a digit in the version tag. For example, the following tuple
1117     would represent version 1.6.6.9:
1118
1119         ('1', '6', '6', '9')
1120     """
1121     global _ver
1122     if not _ver:
1123         p = subprocess.Popen(['git', '--version'],
1124                              stdout=subprocess.PIPE)
1125         gvs = p.stdout.read()
1126         _git_wait('git --version', p)
1127         m = re.match(r'git version (\S+.\S+)', gvs)
1128         if not m:
1129             raise GitError('git --version weird output: %r' % gvs)
1130         _ver = tuple(m.group(1).split('.'))
1131     needed = ('1','5', '3', '1')
1132     if _ver < needed:
1133         raise GitError('git version %s or higher is required; you have %s'
1134                        % ('.'.join(needed), '.'.join(_ver)))
1135     return _ver
1136
1137
1138 class _AbortableIter:
1139     def __init__(self, it, onabort = None):
1140         self.it = it
1141         self.onabort = onabort
1142         self.done = None
1143
1144     def __iter__(self):
1145         return self
1146
1147     def next(self):
1148         try:
1149             return next(self.it)
1150         except StopIteration as e:
1151             self.done = True
1152             raise
1153         except:
1154             self.abort()
1155             raise
1156
1157     def abort(self):
1158         """Abort iteration and call the abortion callback, if needed."""
1159         if not self.done:
1160             self.done = True
1161             if self.onabort:
1162                 self.onabort()
1163
1164     def __del__(self):
1165         self.abort()
1166
1167
1168 class CatPipe:
1169     """Link to 'git cat-file' that is used to retrieve blob data."""
1170     def __init__(self, repo_dir = None):
1171         self.repo_dir = repo_dir
1172         wanted = ('1','5','6')
1173         if ver() < wanted:
1174             log('error: git version must be at least 1.5.6\n')
1175             sys.exit(1)
1176         self.p = self.inprogress = None
1177
1178     def _abort(self):
1179         if self.p:
1180             self.p.stdout.close()
1181             self.p.stdin.close()
1182         self.p = None
1183         self.inprogress = None
1184
1185     def restart(self):
1186         self._abort()
1187         self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
1188                                   stdin=subprocess.PIPE,
1189                                   stdout=subprocess.PIPE,
1190                                   close_fds = True,
1191                                   bufsize = 4096,
1192                                   env=_gitenv(self.repo_dir))
1193
1194     def get(self, ref):
1195         """Yield (oidx, type, size), followed by the data referred to by ref.
1196         If ref does not exist, only yield (None, None, None).
1197
1198         """
1199         if not self.p or self.p.poll() != None:
1200             self.restart()
1201         assert(self.p)
1202         poll_result = self.p.poll()
1203         assert(poll_result == None)
1204         if self.inprogress:
1205             log('get: opening %r while %r is open\n' % (ref, self.inprogress))
1206         assert(not self.inprogress)
1207         assert(ref.find('\n') < 0)
1208         assert(ref.find('\r') < 0)
1209         assert(not ref.startswith('-'))
1210         self.inprogress = ref
1211         self.p.stdin.write('%s\n' % ref)
1212         self.p.stdin.flush()
1213         hdr = self.p.stdout.readline()
1214         if hdr.endswith(' missing\n'):
1215             self.inprogress = None
1216             yield None, None, None
1217             return
1218         info = hdr.split(' ')
1219         if len(info) != 3 or len(info[0]) != 40:
1220             raise GitError('expected object (id, type, size), got %r' % info)
1221         oidx, typ, size = info
1222         size = int(size)
1223         it = _AbortableIter(chunkyreader(self.p.stdout, size),
1224                             onabort=self._abort)
1225         try:
1226             yield oidx, typ, size
1227             for blob in it:
1228                 yield blob
1229             readline_result = self.p.stdout.readline()
1230             assert(readline_result == '\n')
1231             self.inprogress = None
1232         except Exception as e:
1233             it.abort()
1234             raise
1235
1236     def _join(self, it):
1237         _, typ, _ = next(it)
1238         if typ == 'blob':
1239             for blob in it:
1240                 yield blob
1241         elif typ == 'tree':
1242             treefile = ''.join(it)
1243             for (mode, name, sha) in tree_decode(treefile):
1244                 for blob in self.join(sha.encode('hex')):
1245                     yield blob
1246         elif typ == 'commit':
1247             treeline = ''.join(it).split('\n')[0]
1248             assert(treeline.startswith('tree '))
1249             for blob in self.join(treeline[5:]):
1250                 yield blob
1251         else:
1252             raise GitError('invalid object type %r: expected blob/tree/commit'
1253                            % typ)
1254
1255     def join(self, id):
1256         """Generate a list of the content of all blobs that can be reached
1257         from an object.  The hash given in 'id' must point to a blob, a tree
1258         or a commit. The content of all blobs that can be seen from trees or
1259         commits will be added to the list.
1260         """
1261         try:
1262             for d in self._join(self.get(id)):
1263                 yield d
1264         except StopIteration:
1265             log('booger!\n')
1266
1267
1268 _cp = {}
1269
1270 def cp(repo_dir=None):
1271     """Create a CatPipe object or reuse the already existing one."""
1272     global _cp, repodir
1273     if not repo_dir:
1274         repo_dir = repodir or repo()
1275     repo_dir = os.path.abspath(repo_dir)
1276     cp = _cp.get(repo_dir)
1277     if not cp:
1278         cp = CatPipe(repo_dir)
1279         _cp[repo_dir] = cp
1280     return cp
1281
1282
1283 def tags(repo_dir = None):
1284     """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1285     tags = {}
1286     for n, c in list_refs(repo_dir = repo_dir, limit_to_tags=True):
1287         assert(n.startswith('refs/tags/'))
1288         name = n[10:]
1289         if not c in tags:
1290             tags[c] = []
1291         tags[c].append(name)  # more than one tag can point at 'c'
1292     return tags
1293
1294
1295 class MissingObject(KeyError):
1296     def __init__(self, oid):
1297         self.oid = oid
1298         KeyError.__init__(self, 'object %r is missing' % oid.encode('hex'))
1299
1300
1301 WalkItem = namedtuple('WalkItem', ['oid', 'type', 'mode',
1302                                    'path', 'chunk_path', 'data'])
1303 # The path is the mangled path, and if an item represents a fragment
1304 # of a chunked file, the chunk_path will be the chunked subtree path
1305 # for the chunk, i.e. ['', '2d3115e', ...].  The top-level path for a
1306 # chunked file will have a chunk_path of [''].  So some chunk subtree
1307 # of the file '/foo/bar/baz' might look like this:
1308 #
1309 #   item.path = ['foo', 'bar', 'baz.bup']
1310 #   item.chunk_path = ['', '2d3115e', '016b097']
1311 #   item.type = 'tree'
1312 #   ...
1313
1314
1315 def walk_object(get_ref, oidx, stop_at=None, include_data=None):
1316     """Yield everything reachable from oidx via get_ref (which must behave
1317     like CatPipe get) as a WalkItem, stopping whenever stop_at(oidx)
1318     returns true.  Throw MissingObject if a hash encountered is
1319     missing from the repository, and don't read or return blob content
1320     in the data field unless include_data is set.
1321
1322     """
1323     # Maintain the pending stack on the heap to avoid stack overflow
1324     pending = [(oidx, [], [], None)]
1325     while len(pending):
1326         oidx, parent_path, chunk_path, mode = pending.pop()
1327         oid = oidx.decode('hex')
1328         if stop_at and stop_at(oidx):
1329             continue
1330
1331         if (not include_data) and mode and stat.S_ISREG(mode):
1332             # If the object is a "regular file", then it's a leaf in
1333             # the graph, so we can skip reading the data if the caller
1334             # hasn't requested it.
1335             yield WalkItem(oid=oid, type='blob',
1336                            chunk_path=chunk_path, path=parent_path,
1337                            mode=mode,
1338                            data=None)
1339             continue
1340
1341         item_it = get_ref(oidx)
1342         get_oidx, typ, _ = next(item_it)
1343         if not get_oidx:
1344             raise MissingObject(oidx.decode('hex'))
1345         if typ not in ('blob', 'commit', 'tree'):
1346             raise Exception('unexpected repository object type %r' % typ)
1347
1348         # FIXME: set the mode based on the type when the mode is None
1349         if typ == 'blob' and not include_data:
1350             # Dump data until we can ask cat_pipe not to fetch it
1351             for ignored in item_it:
1352                 pass
1353             data = None
1354         else:
1355             data = ''.join(item_it)
1356
1357         yield WalkItem(oid=oid, type=typ,
1358                        chunk_path=chunk_path, path=parent_path,
1359                        mode=mode,
1360                        data=(data if include_data else None))
1361
1362         if typ == 'commit':
1363             commit_items = parse_commit(data)
1364             for pid in commit_items.parents:
1365                 pending.append((pid, parent_path, chunk_path, mode))
1366             pending.append((commit_items.tree, parent_path, chunk_path,
1367                             hashsplit.GIT_MODE_TREE))
1368         elif typ == 'tree':
1369             for mode, name, ent_id in tree_decode(data):
1370                 demangled, bup_type = demangle_name(name, mode)
1371                 if chunk_path:
1372                     sub_path = parent_path
1373                     sub_chunk_path = chunk_path + [name]
1374                 else:
1375                     sub_path = parent_path + [name]
1376                     if bup_type == BUP_CHUNKED:
1377                         sub_chunk_path = ['']
1378                     else:
1379                         sub_chunk_path = chunk_path
1380                 pending.append((ent_id.encode('hex'), sub_path, sub_chunk_path,
1381                                 mode))