1 """Git interaction library.
2 bup repositories are in Git format. This library allows us to
3 interact with the Git data structures.
5 import os, sys, zlib, time, subprocess, struct, stat, re, tempfile, glob
6 from bup.helpers import *
7 from bup import _helpers, path, midx, bloom, xstat
9 max_pack_size = 1000*1000*1000 # larger packs will slow down pruning
10 max_pack_objects = 200*1000 # cache memory usage is about 83 bytes per object
16 _typemap = { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
17 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
23 class GitError(Exception):
28 """Get the path to the git repository or one of its subdirectories."""
31 raise GitError('You should call check_repo_or_die()')
33 # If there's a .git subdirectory, then the actual repo is in there.
34 gd = os.path.join(repodir, '.git')
35 if os.path.exists(gd):
38 return os.path.join(repodir, sub)
42 return re.sub(r'([^0-9a-z]|\b)([0-9a-z]{7})[0-9a-z]{33}([^0-9a-z]|\b)',
47 full = os.path.abspath(path)
48 fullrepo = os.path.abspath(repo(''))
49 if not fullrepo.endswith('/'):
51 if full.startswith(fullrepo):
52 path = full[len(fullrepo):]
53 if path.startswith('index-cache/'):
54 path = path[len('index-cache/'):]
55 return shorten_hash(path)
59 paths = [repo('objects/pack')]
60 paths += glob.glob(repo('index-cache/*/.'))
64 def auto_midx(objdir):
65 args = [path.exe(), 'midx', '--auto', '--dir', objdir]
67 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
69 # make sure 'args' gets printed to help with debugging
70 add_error('%r: exception: %s' % (args, e))
73 add_error('%r: returned %d' % (args, rv))
75 args = [path.exe(), 'bloom', '--dir', objdir]
77 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
79 # make sure 'args' gets printed to help with debugging
80 add_error('%r: exception: %s' % (args, e))
83 add_error('%r: returned %d' % (args, rv))
86 def mangle_name(name, mode, gitmode):
87 """Mangle a file name to present an abstract name for segmented files.
88 Mangled file names will have the ".bup" extension added to them. If a
89 file's name already ends with ".bup", a ".bupl" extension is added to
90 disambiguate normal files from semgmented ones.
92 if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
94 elif name.endswith('.bup') or name[:-1].endswith('.bup'):
100 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
101 def demangle_name(name):
102 """Remove name mangling from a file name, if necessary.
104 The return value is a tuple (demangled_filename,mode), where mode is one of
107 * BUP_NORMAL : files that should be read as-is from the repository
108 * BUP_CHUNKED : files that were chunked and need to be assembled
110 For more information on the name mangling algorythm, see mangle_name()
112 if name.endswith('.bupl'):
113 return (name[:-5], BUP_NORMAL)
114 elif name.endswith('.bup'):
115 return (name[:-4], BUP_CHUNKED)
117 return (name, BUP_NORMAL)
120 def calc_hash(type, content):
121 """Calculate some content's hash in the Git fashion."""
122 header = '%s %d\0' % (type, len(content))
128 def shalist_item_sort_key(ent):
129 (mode, name, id) = ent
130 assert(mode+0 == mode)
131 if stat.S_ISDIR(mode):
137 def tree_encode(shalist):
138 """Generate a git tree object from (mode,name,hash) tuples."""
139 shalist = sorted(shalist, key = shalist_item_sort_key)
141 for (mode,name,bin) in shalist:
143 assert(mode+0 == mode)
145 assert(len(bin) == 20)
146 s = '%o %s\0%s' % (mode,name,bin)
147 assert(s[0] != '0') # 0-padded octal is not acceptable in a git tree
152 def tree_decode(buf):
153 """Generate a list of (mode,name,hash) from the git tree object in buf."""
155 while ofs < len(buf):
156 z = buf.find('\0', ofs)
158 spl = buf[ofs:z].split(' ', 1)
159 assert(len(spl) == 2)
161 sha = buf[z+1:z+1+20]
163 yield (int(mode, 8), name, sha)
166 def _encode_packobj(type, content, compression_level=1):
169 szbits = (sz & 0x0f) | (_typemap[type]<<4)
172 if sz: szbits |= 0x80
178 if compression_level > 9:
179 compression_level = 9
180 elif compression_level < 0:
181 compression_level = 0
182 z = zlib.compressobj(compression_level)
184 yield z.compress(content)
188 def _encode_looseobj(type, content, compression_level=1):
189 z = zlib.compressobj(compression_level)
190 yield z.compress('%s %d\0' % (type, len(content)))
191 yield z.compress(content)
195 def _decode_looseobj(buf):
197 s = zlib.decompress(buf)
204 assert(type in _typemap)
205 assert(sz == len(content))
206 return (type, content)
209 def _decode_packobj(buf):
212 type = _typermap[(c & 0x70) >> 4]
219 sz |= (c & 0x7f) << shift
223 return (type, zlib.decompress(buf[i+1:]))
230 def find_offset(self, hash):
231 """Get the offset of an object inside the index file."""
232 idx = self._idx_from_hash(hash)
234 return self._ofs_from_idx(idx)
237 def exists(self, hash, want_source=False):
238 """Return nonempty if the object exists in this index."""
239 if hash and (self._idx_from_hash(hash) != None):
240 return want_source and os.path.basename(self.name) or True
244 return int(self.fanout[255])
246 def _idx_from_hash(self, hash):
247 global _total_searches, _total_steps
249 assert(len(hash) == 20)
251 start = self.fanout[b1-1] # range -1..254
252 end = self.fanout[b1] # range 0..255
254 _total_steps += 1 # lookup table is a step
257 mid = start + (end-start)/2
258 v = self._idx_to_hash(mid)
268 class PackIdxV1(PackIdx):
269 """Object representation of a Git pack index (version 1) file."""
270 def __init__(self, filename, f):
272 self.idxnames = [self.name]
273 self.map = mmap_read(f)
274 self.fanout = list(struct.unpack('!256I',
275 str(buffer(self.map, 0, 256*4))))
276 self.fanout.append(0) # entry "-1"
277 nsha = self.fanout[255]
279 self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
281 def _ofs_from_idx(self, idx):
282 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
284 def _idx_to_hash(self, idx):
285 return str(self.shatable[idx*24+4 : idx*24+24])
288 for i in xrange(self.fanout[255]):
289 yield buffer(self.map, 256*4 + 24*i + 4, 20)
292 class PackIdxV2(PackIdx):
293 """Object representation of a Git pack index (version 2) file."""
294 def __init__(self, filename, f):
296 self.idxnames = [self.name]
297 self.map = mmap_read(f)
298 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
299 self.fanout = list(struct.unpack('!256I',
300 str(buffer(self.map, 8, 256*4))))
301 self.fanout.append(0) # entry "-1"
302 nsha = self.fanout[255]
303 self.sha_ofs = 8 + 256*4
304 self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
305 self.ofstable = buffer(self.map,
306 self.sha_ofs + nsha*20 + nsha*4,
308 self.ofs64table = buffer(self.map,
309 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
311 def _ofs_from_idx(self, idx):
312 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
314 idx64 = ofs & 0x7fffffff
315 ofs = struct.unpack('!Q',
316 str(buffer(self.ofs64table, idx64*8, 8)))[0]
319 def _idx_to_hash(self, idx):
320 return str(self.shatable[idx*20:(idx+1)*20])
323 for i in xrange(self.fanout[255]):
324 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
329 def __init__(self, dir):
331 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
336 self.do_bloom = False
343 assert(_mpi_count == 0)
346 return iter(idxmerge(self.packs))
349 return sum(len(pack) for pack in self.packs)
351 def exists(self, hash, want_source=False):
352 """Return nonempty if the object exists in the index files."""
353 global _total_searches
355 if hash in self.also:
357 if self.do_bloom and self.bloom:
358 if self.bloom.exists(hash):
359 self.do_bloom = False
361 _total_searches -= 1 # was counted by bloom
363 for i in xrange(len(self.packs)):
365 _total_searches -= 1 # will be incremented by sub-pack
366 ix = p.exists(hash, want_source=want_source)
368 # reorder so most recently used packs are searched first
369 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
374 def refresh(self, skip_midx = False):
375 """Refresh the index list.
376 This method verifies if .midx files were superseded (e.g. all of its
377 contents are in another, bigger .midx file) and removes the superseded
380 If skip_midx is True, all work on .midx files will be skipped and .midx
381 files will be removed from the list.
383 The module-global variable 'ignore_midx' can force this function to
384 always act as if skip_midx was True.
386 self.bloom = None # Always reopen the bloom as it may have been relaced
387 self.do_bloom = False
388 skip_midx = skip_midx or ignore_midx
389 d = dict((p.name, p) for p in self.packs
390 if not skip_midx or not isinstance(p, midx.PackMidx))
391 if os.path.exists(self.dir):
394 for ix in self.packs:
395 if isinstance(ix, midx.PackMidx):
396 for name in ix.idxnames:
397 d[os.path.join(self.dir, name)] = ix
398 for full in glob.glob(os.path.join(self.dir,'*.midx')):
400 mx = midx.PackMidx(full)
401 (mxd, mxf) = os.path.split(mx.name)
403 for n in mx.idxnames:
404 if not os.path.exists(os.path.join(mxd, n)):
405 log(('warning: index %s missing\n' +
406 ' used by %s\n') % (n, mxf))
413 midxl.sort(key=lambda ix:
414 (-len(ix), -xstat.stat(ix.name).st_mtime))
417 for sub in ix.idxnames:
418 found = d.get(os.path.join(self.dir, sub))
419 if not found or isinstance(found, PackIdx):
420 # doesn't exist, or exists but not in a midx
425 for name in ix.idxnames:
426 d[os.path.join(self.dir, name)] = ix
427 elif not ix.force_keep:
428 debug1('midx: removing redundant: %s\n'
429 % os.path.basename(ix.name))
431 for full in glob.glob(os.path.join(self.dir,'*.idx')):
439 bfull = os.path.join(self.dir, 'bup.bloom')
440 if self.bloom is None and os.path.exists(bfull):
441 self.bloom = bloom.ShaBloom(bfull)
442 self.packs = list(set(d.values()))
443 self.packs.sort(lambda x,y: -cmp(len(x),len(y)))
444 if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
448 debug1('PackIdxList: using %d index%s.\n'
449 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
452 """Insert an additional object in the list."""
456 def open_idx(filename):
457 if filename.endswith('.idx'):
458 f = open(filename, 'rb')
460 if header[0:4] == '\377tOc':
461 version = struct.unpack('!I', header[4:8])[0]
463 return PackIdxV2(filename, f)
465 raise GitError('%s: expected idx file version 2, got %d'
466 % (filename, version))
467 elif len(header) == 8 and header[0:4] < '\377tOc':
468 return PackIdxV1(filename, f)
470 raise GitError('%s: unrecognized idx file header' % filename)
471 elif filename.endswith('.midx'):
472 return midx.PackMidx(filename)
474 raise GitError('idx filenames must end with .idx or .midx')
477 def idxmerge(idxlist, final_progress=True):
478 """Generate a list of all the objects reachable in a PackIdxList."""
479 def pfunc(count, total):
480 qprogress('Reading indexes: %.2f%% (%d/%d)\r'
481 % (count*100.0/total, count, total))
482 def pfinal(count, total):
484 progress('Reading indexes: %.2f%% (%d/%d), done.\n'
485 % (100, total, total))
486 return merge_iter(idxlist, 10024, pfunc, pfinal)
489 def _make_objcache():
490 return PackIdxList(repo('objects/pack'))
493 """Writes Git objects inside a pack file."""
494 def __init__(self, objcache_maker=_make_objcache, compression_level=1):
500 self.objcache_maker = objcache_maker
502 self.compression_level = compression_level
509 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
510 self.file = os.fdopen(fd, 'w+b')
511 assert(name.endswith('.pack'))
512 self.filename = name[:-5]
513 self.file.write('PACK\0\0\0\2\0\0\0\0')
514 self.idx = list(list() for i in xrange(256))
516 def _raw_write(self, datalist, sha):
519 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
520 # the file never has a *partial* blob. So let's make sure it's
521 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
522 # to our hashsplit algorithm.) f.write() does its own buffering,
523 # but that's okay because we'll flush it in _end().
524 oneblob = ''.join(datalist)
528 raise GitError, e, sys.exc_info()[2]
530 crc = zlib.crc32(oneblob) & 0xffffffff
531 self._update_idx(sha, crc, nw)
536 def _update_idx(self, sha, crc, size):
539 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
541 def _write(self, sha, type, content):
545 sha = calc_hash(type, content)
546 size, crc = self._raw_write(_encode_packobj(type, content,
547 self.compression_level),
549 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
553 def breakpoint(self):
554 """Clear byte and object counts and return the last processed id."""
556 self.outbytes = self.count = 0
559 def _require_objcache(self):
560 if self.objcache is None and self.objcache_maker:
561 self.objcache = self.objcache_maker()
562 if self.objcache is None:
564 "PackWriter not opened or can't check exists w/o objcache")
566 def exists(self, id, want_source=False):
567 """Return non-empty if an object is found in the object cache."""
568 self._require_objcache()
569 return self.objcache.exists(id, want_source=want_source)
571 def maybe_write(self, type, content):
572 """Write an object to the pack file if not present and return its id."""
573 sha = calc_hash(type, content)
574 if not self.exists(sha):
575 self._write(sha, type, content)
576 self._require_objcache()
577 self.objcache.add(sha)
580 def new_blob(self, blob):
581 """Create a blob object in the pack with the supplied content."""
582 return self.maybe_write('blob', blob)
584 def new_tree(self, shalist):
585 """Create a tree object in the pack."""
586 content = tree_encode(shalist)
587 return self.maybe_write('tree', content)
589 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
591 if tree: l.append('tree %s' % tree.encode('hex'))
592 if parent: l.append('parent %s' % parent.encode('hex'))
593 if author: l.append('author %s %s' % (author, _git_date(adate)))
594 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
597 return self.maybe_write('commit', '\n'.join(l))
599 def new_commit(self, parent, tree, date, msg):
600 """Create a commit object in the pack."""
601 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
602 commit = self._new_commit(tree, parent,
603 userline, date, userline, date,
608 """Remove the pack file from disk."""
614 os.unlink(self.filename + '.pack')
616 def _end(self, run_midx=True):
618 if not f: return None
624 # update object count
626 cp = struct.pack('!i', self.count)
630 # calculate the pack sha1sum
633 for b in chunkyreader(f):
635 packbin = sum.digest()
639 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
641 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
642 if os.path.exists(self.filename + '.map'):
643 os.unlink(self.filename + '.map')
644 os.rename(self.filename + '.pack', nameprefix + '.pack')
645 os.rename(self.filename + '.idx', nameprefix + '.idx')
648 auto_midx(repo('objects/pack'))
651 def close(self, run_midx=True):
652 """Close the pack file and move it to its definitive path."""
653 return self._end(run_midx=run_midx)
655 def _write_pack_idx_v2(self, filename, idx, packbin):
658 for entry in section:
659 if entry[2] >= 2**31:
662 # Length: header + fan-out + shas-and-crcs + overflow-offsets
663 index_len = 8 + (4 * 256) + (28 * self.count) + (8 * ofs64_count)
665 idx_f = open(filename, 'w+b')
667 idx_f.truncate(index_len)
668 idx_map = mmap_readwrite(idx_f, close=False)
669 count = _helpers.write_idx(filename, idx_map, idx, self.count)
670 assert(count == self.count)
672 if idx_map: idx_map.close()
675 idx_f = open(filename, 'a+b')
680 b = idx_f.read(8 + 4*256)
683 obj_list_sum = Sha1()
684 for b in chunkyreader(idx_f, 20*self.count):
686 obj_list_sum.update(b)
687 namebase = obj_list_sum.hexdigest()
689 for b in chunkyreader(idx_f):
691 idx_f.write(idx_sum.digest())
698 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
702 os.environ['GIT_DIR'] = os.path.abspath(repo())
705 def list_refs(refname = None):
706 """Generate a list of tuples in the form (refname,hash).
707 If a ref name is specified, list only this particular ref.
709 argv = ['git', 'show-ref', '--']
712 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
713 out = p.stdout.read().strip()
714 rv = p.wait() # not fatal
718 for d in out.split('\n'):
719 (sha, name) = d.split(' ', 1)
720 yield (name, sha.decode('hex'))
723 def read_ref(refname):
724 """Get the commit id of the most recent commit made on a given ref."""
725 l = list(list_refs(refname))
733 def rev_list(ref, count=None):
734 """Generate a list of reachable commits in reverse chronological order.
736 This generator walks through commits, from child to parent, that are
737 reachable via the specified ref and yields a series of tuples of the form
740 If count is a non-zero integer, limit the number of commits to "count"
743 assert(not ref.startswith('-'))
746 opts += ['-n', str(atoi(count))]
747 argv = ['git', 'rev-list', '--pretty=format:%at'] + opts + [ref, '--']
748 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
752 if s.startswith('commit '):
753 commit = s[7:].decode('hex')
757 rv = p.wait() # not fatal
759 raise GitError, 'git rev-list returned error %d' % rv
762 def get_commit_dates(refs):
763 """Get the dates for the specified commit refs."""
765 cmd = ['git', 'show', '-s', '--pretty=format:%ct']
766 for chunk in batchpipe(cmd, refs, preexec_fn=_gitenv):
767 result += [int(x) for x in chunk.splitlines()]
771 def rev_parse(committish):
772 """Resolve the full hash for 'committish', if it exists.
774 Should be roughly equivalent to 'git rev-parse'.
776 Returns the hex value of the hash if it is found, None if 'committish' does
777 not correspond to anything.
779 head = read_ref(committish)
781 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
784 pL = PackIdxList(repo('objects/pack'))
786 if len(committish) == 40:
788 hash = committish.decode('hex')
798 def update_ref(refname, newval, oldval):
799 """Change the commit pointed to by a branch."""
802 assert(refname.startswith('refs/heads/'))
803 p = subprocess.Popen(['git', 'update-ref', refname,
804 newval.encode('hex'), oldval.encode('hex')],
805 preexec_fn = _gitenv)
806 _git_wait('git update-ref', p)
809 def guess_repo(path=None):
810 """Set the path value in the global variable "repodir".
811 This makes bup look for an existing bup repository, but not fail if a
812 repository doesn't exist. Usually, if you are interacting with a bup
813 repository, you would not be calling this function but using
820 repodir = os.environ.get('BUP_DIR')
822 repodir = os.path.expanduser('~/.bup')
825 def init_repo(path=None):
826 """Create the Git bare repository for bup in a given path."""
828 d = repo() # appends a / to the path
829 parent = os.path.dirname(os.path.dirname(d))
830 if parent and not os.path.exists(parent):
831 raise GitError('parent directory "%s" does not exist\n' % parent)
832 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
833 raise GitError('"%s" exists but is not a directory\n' % d)
834 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
835 preexec_fn = _gitenv)
836 _git_wait('git init', p)
837 # Force the index version configuration in order to ensure bup works
838 # regardless of the version of the installed Git binary.
839 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
840 stdout=sys.stderr, preexec_fn = _gitenv)
841 _git_wait('git config', p)
843 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
844 stdout=sys.stderr, preexec_fn = _gitenv)
845 _git_wait('git config', p)
848 def check_repo_or_die(path=None):
849 """Make sure a bup repository exists, and abort if not.
850 If the path to a particular repository was not specified, this function
851 initializes the default repository automatically.
855 os.stat(repo('objects/pack/.'))
857 if e.errno == errno.ENOENT:
858 log('error: %r is not a bup repository; run "bup init"\n'
862 log('error: %s\n' % e)
868 """Get Git's version and ensure a usable version is installed.
870 The returned version is formatted as an ordered tuple with each position
871 representing a digit in the version tag. For example, the following tuple
872 would represent version 1.6.6.9:
878 p = subprocess.Popen(['git', '--version'],
879 stdout=subprocess.PIPE)
880 gvs = p.stdout.read()
881 _git_wait('git --version', p)
882 m = re.match(r'git version (\S+.\S+)', gvs)
884 raise GitError('git --version weird output: %r' % gvs)
885 _ver = tuple(m.group(1).split('.'))
886 needed = ('1','5', '3', '1')
888 raise GitError('git version %s or higher is required; you have %s'
889 % ('.'.join(needed), '.'.join(_ver)))
893 def _git_wait(cmd, p):
896 raise GitError('%s returned %d' % (cmd, rv))
899 def _git_capture(argv):
900 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
902 _git_wait(repr(argv), p)
906 class _AbortableIter:
907 def __init__(self, it, onabort = None):
909 self.onabort = onabort
917 return self.it.next()
918 except StopIteration, e:
926 """Abort iteration and call the abortion callback, if needed."""
938 """Link to 'git cat-file' that is used to retrieve blob data."""
941 wanted = ('1','5','6')
944 log('warning: git version < %s; bup will be slow.\n'
947 self.get = self._slow_get
949 self.p = self.inprogress = None
950 self.get = self._fast_get
954 self.p.stdout.close()
957 self.inprogress = None
961 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
962 stdin=subprocess.PIPE,
963 stdout=subprocess.PIPE,
966 preexec_fn = _gitenv)
968 def _fast_get(self, id):
969 if not self.p or self.p.poll() != None:
972 poll_result = self.p.poll()
973 assert(poll_result == None)
975 log('_fast_get: opening %r while %r is open\n'
976 % (id, self.inprogress))
977 assert(not self.inprogress)
978 assert(id.find('\n') < 0)
979 assert(id.find('\r') < 0)
980 assert(not id.startswith('-'))
982 self.p.stdin.write('%s\n' % id)
984 hdr = self.p.stdout.readline()
985 if hdr.endswith(' missing\n'):
986 self.inprogress = None
987 raise KeyError('blob %r is missing' % id)
989 if len(spl) != 3 or len(spl[0]) != 40:
990 raise GitError('expected blob, got %r' % spl)
991 (hex, type, size) = spl
993 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
994 onabort = self._abort)
999 readline_result = self.p.stdout.readline()
1000 assert(readline_result == '\n')
1001 self.inprogress = None
1002 except Exception, e:
1006 def _slow_get(self, id):
1007 assert(id.find('\n') < 0)
1008 assert(id.find('\r') < 0)
1009 assert(id[0] != '-')
1010 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1013 p = subprocess.Popen(['git', 'cat-file', type, id],
1014 stdout=subprocess.PIPE,
1015 preexec_fn = _gitenv)
1016 for blob in chunkyreader(p.stdout):
1018 _git_wait('git cat-file', p)
1020 def _join(self, it):
1025 elif type == 'tree':
1026 treefile = ''.join(it)
1027 for (mode, name, sha) in tree_decode(treefile):
1028 for blob in self.join(sha.encode('hex')):
1030 elif type == 'commit':
1031 treeline = ''.join(it).split('\n')[0]
1032 assert(treeline.startswith('tree '))
1033 for blob in self.join(treeline[5:]):
1036 raise GitError('invalid object type %r: expected blob/tree/commit'
1040 """Generate a list of the content of all blobs that can be reached
1041 from an object. The hash given in 'id' must point to a blob, a tree
1042 or a commit. The content of all blobs that can be seen from trees or
1043 commits will be added to the list.
1046 for d in self._join(self.get(id)):
1048 except StopIteration:
1052 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1054 for (n,c) in list_refs():
1055 if n.startswith('refs/tags/'):
1060 tags[c].append(name) # more than one tag can point at 'c'