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
11 SEEK_END=2 # os.SEEK_END is not defined in python 2.4
15 home_repodir = os.path.expanduser('~/.bup')
18 _typemap = { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
19 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
25 class GitError(Exception):
30 """Get the path to the git repository or one of its subdirectories."""
33 raise GitError('You should call check_repo_or_die()')
35 # If there's a .git subdirectory, then the actual repo is in there.
36 gd = os.path.join(repodir, '.git')
37 if os.path.exists(gd):
40 return os.path.join(repodir, sub)
44 return re.sub(r'([^0-9a-z]|\b)([0-9a-z]{7})[0-9a-z]{33}([^0-9a-z]|\b)',
49 full = os.path.abspath(path)
50 fullrepo = os.path.abspath(repo(''))
51 if not fullrepo.endswith('/'):
53 if full.startswith(fullrepo):
54 path = full[len(fullrepo):]
55 if path.startswith('index-cache/'):
56 path = path[len('index-cache/'):]
57 return shorten_hash(path)
61 paths = [repo('objects/pack')]
62 paths += glob.glob(repo('index-cache/*/.'))
66 def auto_midx(objdir):
67 args = [path.exe(), 'midx', '--auto', '--dir', objdir]
69 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
71 # make sure 'args' gets printed to help with debugging
72 add_error('%r: exception: %s' % (args, e))
75 add_error('%r: returned %d' % (args, rv))
77 args = [path.exe(), 'bloom', '--dir', objdir]
79 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
81 # make sure 'args' gets printed to help with debugging
82 add_error('%r: exception: %s' % (args, e))
85 add_error('%r: returned %d' % (args, rv))
88 def mangle_name(name, mode, gitmode):
89 """Mangle a file name to present an abstract name for segmented files.
90 Mangled file names will have the ".bup" extension added to them. If a
91 file's name already ends with ".bup", a ".bupl" extension is added to
92 disambiguate normal files from semgmented ones.
94 if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
96 elif name.endswith('.bup') or name[:-1].endswith('.bup'):
102 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
103 def demangle_name(name):
104 """Remove name mangling from a file name, if necessary.
106 The return value is a tuple (demangled_filename,mode), where mode is one of
109 * BUP_NORMAL : files that should be read as-is from the repository
110 * BUP_CHUNKED : files that were chunked and need to be assembled
112 For more information on the name mangling algorythm, see mangle_name()
114 if name.endswith('.bupl'):
115 return (name[:-5], BUP_NORMAL)
116 elif name.endswith('.bup'):
117 return (name[:-4], BUP_CHUNKED)
119 return (name, BUP_NORMAL)
122 def calc_hash(type, content):
123 """Calculate some content's hash in the Git fashion."""
124 header = '%s %d\0' % (type, len(content))
130 def shalist_item_sort_key(ent):
131 (mode, name, id) = ent
132 assert(mode+0 == mode)
133 if stat.S_ISDIR(mode):
139 def tree_encode(shalist):
140 """Generate a git tree object from (mode,name,hash) tuples."""
141 shalist = sorted(shalist, key = shalist_item_sort_key)
143 for (mode,name,bin) in shalist:
145 assert(mode+0 == mode)
147 assert(len(bin) == 20)
148 s = '%o %s\0%s' % (mode,name,bin)
149 assert(s[0] != '0') # 0-padded octal is not acceptable in a git tree
154 def tree_decode(buf):
155 """Generate a list of (mode,name,hash) from the git tree object in buf."""
157 while ofs < len(buf):
158 z = buf.find('\0', ofs)
160 spl = buf[ofs:z].split(' ', 1)
161 assert(len(spl) == 2)
163 sha = buf[z+1:z+1+20]
165 yield (int(mode, 8), name, sha)
168 def _encode_packobj(type, content, compression_level=1):
171 szbits = (sz & 0x0f) | (_typemap[type]<<4)
174 if sz: szbits |= 0x80
180 if compression_level > 9:
181 compression_level = 9
182 elif compression_level < 0:
183 compression_level = 0
184 z = zlib.compressobj(compression_level)
186 yield z.compress(content)
190 def _encode_looseobj(type, content, compression_level=1):
191 z = zlib.compressobj(compression_level)
192 yield z.compress('%s %d\0' % (type, len(content)))
193 yield z.compress(content)
197 def _decode_looseobj(buf):
199 s = zlib.decompress(buf)
206 assert(type in _typemap)
207 assert(sz == len(content))
208 return (type, content)
211 def _decode_packobj(buf):
214 type = _typermap[(c & 0x70) >> 4]
221 sz |= (c & 0x7f) << shift
225 return (type, zlib.decompress(buf[i+1:]))
232 def find_offset(self, hash):
233 """Get the offset of an object inside the index file."""
234 idx = self._idx_from_hash(hash)
236 return self._ofs_from_idx(idx)
239 def exists(self, hash, want_source=False):
240 """Return nonempty if the object exists in this index."""
241 if hash and (self._idx_from_hash(hash) != None):
242 return want_source and os.path.basename(self.name) or True
246 return int(self.fanout[255])
248 def _idx_from_hash(self, hash):
249 global _total_searches, _total_steps
251 assert(len(hash) == 20)
253 start = self.fanout[b1-1] # range -1..254
254 end = self.fanout[b1] # range 0..255
256 _total_steps += 1 # lookup table is a step
259 mid = start + (end-start)/2
260 v = self._idx_to_hash(mid)
270 class PackIdxV1(PackIdx):
271 """Object representation of a Git pack index (version 1) file."""
272 def __init__(self, filename, f):
274 self.idxnames = [self.name]
275 self.map = mmap_read(f)
276 self.fanout = list(struct.unpack('!256I',
277 str(buffer(self.map, 0, 256*4))))
278 self.fanout.append(0) # entry "-1"
279 nsha = self.fanout[255]
281 self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
283 def _ofs_from_idx(self, idx):
284 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
286 def _idx_to_hash(self, idx):
287 return str(self.shatable[idx*24+4 : idx*24+24])
290 for i in xrange(self.fanout[255]):
291 yield buffer(self.map, 256*4 + 24*i + 4, 20)
294 class PackIdxV2(PackIdx):
295 """Object representation of a Git pack index (version 2) file."""
296 def __init__(self, filename, f):
298 self.idxnames = [self.name]
299 self.map = mmap_read(f)
300 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
301 self.fanout = list(struct.unpack('!256I',
302 str(buffer(self.map, 8, 256*4))))
303 self.fanout.append(0) # entry "-1"
304 nsha = self.fanout[255]
305 self.sha_ofs = 8 + 256*4
306 self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
307 self.ofstable = buffer(self.map,
308 self.sha_ofs + nsha*20 + nsha*4,
310 self.ofs64table = buffer(self.map,
311 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
313 def _ofs_from_idx(self, idx):
314 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
316 idx64 = ofs & 0x7fffffff
317 ofs = struct.unpack('!Q',
318 str(buffer(self.ofs64table, idx64*8, 8)))[0]
321 def _idx_to_hash(self, idx):
322 return str(self.shatable[idx*20:(idx+1)*20])
325 for i in xrange(self.fanout[255]):
326 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
331 def __init__(self, dir):
333 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
338 self.do_bloom = False
345 assert(_mpi_count == 0)
348 return iter(idxmerge(self.packs))
351 return sum(len(pack) for pack in self.packs)
353 def exists(self, hash, want_source=False):
354 """Return nonempty if the object exists in the index files."""
355 global _total_searches
357 if hash in self.also:
359 if self.do_bloom and self.bloom:
360 if self.bloom.exists(hash):
361 self.do_bloom = False
363 _total_searches -= 1 # was counted by bloom
365 for i in xrange(len(self.packs)):
367 _total_searches -= 1 # will be incremented by sub-pack
368 ix = p.exists(hash, want_source=want_source)
370 # reorder so most recently used packs are searched first
371 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
376 def refresh(self, skip_midx = False):
377 """Refresh the index list.
378 This method verifies if .midx files were superseded (e.g. all of its
379 contents are in another, bigger .midx file) and removes the superseded
382 If skip_midx is True, all work on .midx files will be skipped and .midx
383 files will be removed from the list.
385 The module-global variable 'ignore_midx' can force this function to
386 always act as if skip_midx was True.
388 self.bloom = None # Always reopen the bloom as it may have been relaced
389 self.do_bloom = False
390 skip_midx = skip_midx or ignore_midx
391 d = dict((p.name, p) for p in self.packs
392 if not skip_midx or not isinstance(p, midx.PackMidx))
393 if os.path.exists(self.dir):
396 for ix in self.packs:
397 if isinstance(ix, midx.PackMidx):
398 for name in ix.idxnames:
399 d[os.path.join(self.dir, name)] = ix
400 for full in glob.glob(os.path.join(self.dir,'*.midx')):
402 mx = midx.PackMidx(full)
403 (mxd, mxf) = os.path.split(mx.name)
405 for n in mx.idxnames:
406 if not os.path.exists(os.path.join(mxd, n)):
407 log(('warning: index %s missing\n' +
408 ' used by %s\n') % (n, mxf))
415 midxl.sort(key=lambda ix:
416 (-len(ix), -xstat.stat(ix.name).st_mtime))
419 for sub in ix.idxnames:
420 found = d.get(os.path.join(self.dir, sub))
421 if not found or isinstance(found, PackIdx):
422 # doesn't exist, or exists but not in a midx
427 for name in ix.idxnames:
428 d[os.path.join(self.dir, name)] = ix
429 elif not ix.force_keep:
430 debug1('midx: removing redundant: %s\n'
431 % os.path.basename(ix.name))
433 for full in glob.glob(os.path.join(self.dir,'*.idx')):
441 bfull = os.path.join(self.dir, 'bup.bloom')
442 if self.bloom is None and os.path.exists(bfull):
443 self.bloom = bloom.ShaBloom(bfull)
444 self.packs = list(set(d.values()))
445 self.packs.sort(lambda x,y: -cmp(len(x),len(y)))
446 if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
450 debug1('PackIdxList: using %d index%s.\n'
451 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
454 """Insert an additional object in the list."""
458 def open_idx(filename):
459 if filename.endswith('.idx'):
460 f = open(filename, 'rb')
462 if header[0:4] == '\377tOc':
463 version = struct.unpack('!I', header[4:8])[0]
465 return PackIdxV2(filename, f)
467 raise GitError('%s: expected idx file version 2, got %d'
468 % (filename, version))
469 elif len(header) == 8 and header[0:4] < '\377tOc':
470 return PackIdxV1(filename, f)
472 raise GitError('%s: unrecognized idx file header' % filename)
473 elif filename.endswith('.midx'):
474 return midx.PackMidx(filename)
476 raise GitError('idx filenames must end with .idx or .midx')
479 def idxmerge(idxlist, final_progress=True):
480 """Generate a list of all the objects reachable in a PackIdxList."""
481 def pfunc(count, total):
482 qprogress('Reading indexes: %.2f%% (%d/%d)\r'
483 % (count*100.0/total, count, total))
484 def pfinal(count, total):
486 progress('Reading indexes: %.2f%% (%d/%d), done.\n'
487 % (100, total, total))
488 return merge_iter(idxlist, 10024, pfunc, pfinal)
491 def _make_objcache():
492 return PackIdxList(repo('objects/pack'))
495 """Writes Git objects inside a pack file."""
496 def __init__(self, objcache_maker=_make_objcache, compression_level=1):
502 self.objcache_maker = objcache_maker
504 self.compression_level = compression_level
511 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
512 self.file = os.fdopen(fd, 'w+b')
513 assert(name.endswith('.pack'))
514 self.filename = name[:-5]
515 self.file.write('PACK\0\0\0\2\0\0\0\0')
516 self.idx = list(list() for i in xrange(256))
518 def _raw_write(self, datalist, sha):
521 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
522 # the file never has a *partial* blob. So let's make sure it's
523 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
524 # to our hashsplit algorithm.) f.write() does its own buffering,
525 # but that's okay because we'll flush it in _end().
526 oneblob = ''.join(datalist)
530 raise GitError, e, sys.exc_info()[2]
532 crc = zlib.crc32(oneblob) & 0xffffffff
533 self._update_idx(sha, crc, nw)
538 def _update_idx(self, sha, crc, size):
541 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
543 def _write(self, sha, type, content):
547 sha = calc_hash(type, content)
548 size, crc = self._raw_write(_encode_packobj(type, content,
549 self.compression_level),
551 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
555 def breakpoint(self):
556 """Clear byte and object counts and return the last processed id."""
558 self.outbytes = self.count = 0
561 def _require_objcache(self):
562 if self.objcache is None and self.objcache_maker:
563 self.objcache = self.objcache_maker()
564 if self.objcache is None:
566 "PackWriter not opened or can't check exists w/o objcache")
568 def exists(self, id, want_source=False):
569 """Return non-empty if an object is found in the object cache."""
570 self._require_objcache()
571 return self.objcache.exists(id, want_source=want_source)
573 def maybe_write(self, type, content):
574 """Write an object to the pack file if not present and return its id."""
575 sha = calc_hash(type, content)
576 if not self.exists(sha):
577 self._write(sha, type, content)
578 self._require_objcache()
579 self.objcache.add(sha)
582 def new_blob(self, blob):
583 """Create a blob object in the pack with the supplied content."""
584 return self.maybe_write('blob', blob)
586 def new_tree(self, shalist):
587 """Create a tree object in the pack."""
588 content = tree_encode(shalist)
589 return self.maybe_write('tree', content)
591 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
593 if tree: l.append('tree %s' % tree.encode('hex'))
594 if parent: l.append('parent %s' % parent.encode('hex'))
595 if author: l.append('author %s %s' % (author, _git_date(adate)))
596 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
599 return self.maybe_write('commit', '\n'.join(l))
601 def new_commit(self, parent, tree, date, msg):
602 """Create a commit object in the pack."""
603 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
604 commit = self._new_commit(tree, parent,
605 userline, date, userline, date,
610 """Remove the pack file from disk."""
616 os.unlink(self.filename + '.pack')
618 def _end(self, run_midx=True):
620 if not f: return None
626 # update object count
628 cp = struct.pack('!i', self.count)
632 # calculate the pack sha1sum
635 for b in chunkyreader(f):
637 packbin = sum.digest()
641 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
643 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
644 if os.path.exists(self.filename + '.map'):
645 os.unlink(self.filename + '.map')
646 os.rename(self.filename + '.pack', nameprefix + '.pack')
647 os.rename(self.filename + '.idx', nameprefix + '.idx')
650 auto_midx(repo('objects/pack'))
653 def close(self, run_midx=True):
654 """Close the pack file and move it to its definitive path."""
655 return self._end(run_midx=run_midx)
657 def _write_pack_idx_v2(self, filename, idx, packbin):
658 idx_f = open(filename, 'w+b')
659 idx_f.write('\377tOc\0\0\0\2')
661 ofs64_ofs = 8 + 4*256 + 28*self.count
662 idx_f.truncate(ofs64_ofs)
664 idx_map = mmap_readwrite(idx_f, close=False)
665 idx_f.seek(0, SEEK_END)
666 count = _helpers.write_idx(idx_f, idx_map, idx, self.count)
667 assert(count == self.count)
673 b = idx_f.read(8 + 4*256)
676 obj_list_sum = Sha1()
677 for b in chunkyreader(idx_f, 20*self.count):
679 obj_list_sum.update(b)
680 namebase = obj_list_sum.hexdigest()
682 for b in chunkyreader(idx_f):
684 idx_f.write(idx_sum.digest())
691 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
695 os.environ['GIT_DIR'] = os.path.abspath(repo())
698 def list_refs(refname = None):
699 """Generate a list of tuples in the form (refname,hash).
700 If a ref name is specified, list only this particular ref.
702 argv = ['git', 'show-ref', '--']
705 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
706 out = p.stdout.read().strip()
707 rv = p.wait() # not fatal
711 for d in out.split('\n'):
712 (sha, name) = d.split(' ', 1)
713 yield (name, sha.decode('hex'))
716 def read_ref(refname):
717 """Get the commit id of the most recent commit made on a given ref."""
718 l = list(list_refs(refname))
726 def rev_list(ref, count=None):
727 """Generate a list of reachable commits in reverse chronological order.
729 This generator walks through commits, from child to parent, that are
730 reachable via the specified ref and yields a series of tuples of the form
733 If count is a non-zero integer, limit the number of commits to "count"
736 assert(not ref.startswith('-'))
739 opts += ['-n', str(atoi(count))]
740 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
741 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
745 if s.startswith('commit '):
746 commit = s[7:].decode('hex')
750 rv = p.wait() # not fatal
752 raise GitError, 'git rev-list returned error %d' % rv
755 def rev_get_date(ref):
756 """Get the date of the latest commit on the specified ref."""
757 for (date, commit) in rev_list(ref, count=1):
759 raise GitError, 'no such commit %r' % ref
762 def rev_parse(committish):
763 """Resolve the full hash for 'committish', if it exists.
765 Should be roughly equivalent to 'git rev-parse'.
767 Returns the hex value of the hash if it is found, None if 'committish' does
768 not correspond to anything.
770 head = read_ref(committish)
772 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
775 pL = PackIdxList(repo('objects/pack'))
777 if len(committish) == 40:
779 hash = committish.decode('hex')
789 def update_ref(refname, newval, oldval):
790 """Change the commit pointed to by a branch."""
793 assert(refname.startswith('refs/heads/'))
794 p = subprocess.Popen(['git', 'update-ref', refname,
795 newval.encode('hex'), oldval.encode('hex')],
796 preexec_fn = _gitenv)
797 _git_wait('git update-ref', p)
800 def guess_repo(path=None):
801 """Set the path value in the global variable "repodir".
802 This makes bup look for an existing bup repository, but not fail if a
803 repository doesn't exist. Usually, if you are interacting with a bup
804 repository, you would not be calling this function but using
811 repodir = os.environ.get('BUP_DIR')
813 repodir = os.path.expanduser('~/.bup')
816 def init_repo(path=None):
817 """Create the Git bare repository for bup in a given path."""
819 d = repo() # appends a / to the path
820 parent = os.path.dirname(os.path.dirname(d))
821 if parent and not os.path.exists(parent):
822 raise GitError('parent directory "%s" does not exist\n' % parent)
823 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
824 raise GitError('"%s" exists but is not a directory\n' % d)
825 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
826 preexec_fn = _gitenv)
827 _git_wait('git init', p)
828 # Force the index version configuration in order to ensure bup works
829 # regardless of the version of the installed Git binary.
830 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
831 stdout=sys.stderr, preexec_fn = _gitenv)
832 _git_wait('git config', p)
834 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
835 stdout=sys.stderr, preexec_fn = _gitenv)
836 _git_wait('git config', p)
839 def check_repo_or_die(path=None):
840 """Make sure a bup repository exists, and abort if not.
841 If the path to a particular repository was not specified, this function
842 initializes the default repository automatically.
846 os.stat(repo('objects/pack/.'))
848 if e.errno == errno.ENOENT:
849 if repodir != home_repodir:
850 log('error: %r is not a bup repository; run "bup init"\n'
856 log('error: %s\n' % e)
862 """Get Git's version and ensure a usable version is installed.
864 The returned version is formatted as an ordered tuple with each position
865 representing a digit in the version tag. For example, the following tuple
866 would represent version 1.6.6.9:
872 p = subprocess.Popen(['git', '--version'],
873 stdout=subprocess.PIPE)
874 gvs = p.stdout.read()
875 _git_wait('git --version', p)
876 m = re.match(r'git version (\S+.\S+)', gvs)
878 raise GitError('git --version weird output: %r' % gvs)
879 _ver = tuple(m.group(1).split('.'))
880 needed = ('1','5', '3', '1')
882 raise GitError('git version %s or higher is required; you have %s'
883 % ('.'.join(needed), '.'.join(_ver)))
887 def _git_wait(cmd, p):
890 raise GitError('%s returned %d' % (cmd, rv))
893 def _git_capture(argv):
894 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
896 _git_wait(repr(argv), p)
900 class _AbortableIter:
901 def __init__(self, it, onabort = None):
903 self.onabort = onabort
911 return self.it.next()
912 except StopIteration, e:
920 """Abort iteration and call the abortion callback, if needed."""
932 """Link to 'git cat-file' that is used to retrieve blob data."""
935 wanted = ('1','5','6')
938 log('warning: git version < %s; bup will be slow.\n'
941 self.get = self._slow_get
943 self.p = self.inprogress = None
944 self.get = self._fast_get
948 self.p.stdout.close()
951 self.inprogress = None
955 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
956 stdin=subprocess.PIPE,
957 stdout=subprocess.PIPE,
960 preexec_fn = _gitenv)
962 def _fast_get(self, id):
963 if not self.p or self.p.poll() != None:
966 assert(self.p.poll() == None)
968 log('_fast_get: opening %r while %r is open\n'
969 % (id, self.inprogress))
970 assert(not self.inprogress)
971 assert(id.find('\n') < 0)
972 assert(id.find('\r') < 0)
973 assert(not id.startswith('-'))
975 self.p.stdin.write('%s\n' % id)
977 hdr = self.p.stdout.readline()
978 if hdr.endswith(' missing\n'):
979 self.inprogress = None
980 raise KeyError('blob %r is missing' % id)
982 if len(spl) != 3 or len(spl[0]) != 40:
983 raise GitError('expected blob, got %r' % spl)
984 (hex, type, size) = spl
986 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
987 onabort = self._abort)
992 assert(self.p.stdout.readline() == '\n')
993 self.inprogress = None
998 def _slow_get(self, id):
999 assert(id.find('\n') < 0)
1000 assert(id.find('\r') < 0)
1001 assert(id[0] != '-')
1002 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1005 p = subprocess.Popen(['git', 'cat-file', type, id],
1006 stdout=subprocess.PIPE,
1007 preexec_fn = _gitenv)
1008 for blob in chunkyreader(p.stdout):
1010 _git_wait('git cat-file', p)
1012 def _join(self, it):
1017 elif type == 'tree':
1018 treefile = ''.join(it)
1019 for (mode, name, sha) in tree_decode(treefile):
1020 for blob in self.join(sha.encode('hex')):
1022 elif type == 'commit':
1023 treeline = ''.join(it).split('\n')[0]
1024 assert(treeline.startswith('tree '))
1025 for blob in self.join(treeline[5:]):
1028 raise GitError('invalid object type %r: expected blob/tree/commit'
1032 """Generate a list of the content of all blobs that can be reached
1033 from an object. The hash given in 'id' must point to a blob, a tree
1034 or a commit. The content of all blobs that can be seen from trees or
1035 commits will be added to the list.
1038 for d in self._join(self.get(id)):
1040 except StopIteration:
1044 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1046 for (n,c) in list_refs():
1047 if n.startswith('refs/tags/'):
1052 tags[c].append(name) # more than one tag can point at 'c'