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:%ct'] + 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 rev_get_date(ref):
763 """Get the date of the latest commit on the specified ref."""
764 for (date, commit) in rev_list(ref, count=1):
766 raise GitError, 'no such commit %r' % ref
769 def rev_parse(committish):
770 """Resolve the full hash for 'committish', if it exists.
772 Should be roughly equivalent to 'git rev-parse'.
774 Returns the hex value of the hash if it is found, None if 'committish' does
775 not correspond to anything.
777 head = read_ref(committish)
779 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
782 pL = PackIdxList(repo('objects/pack'))
784 if len(committish) == 40:
786 hash = committish.decode('hex')
796 def update_ref(refname, newval, oldval):
797 """Change the commit pointed to by a branch."""
800 assert(refname.startswith('refs/heads/'))
801 p = subprocess.Popen(['git', 'update-ref', refname,
802 newval.encode('hex'), oldval.encode('hex')],
803 preexec_fn = _gitenv)
804 _git_wait('git update-ref', p)
807 def guess_repo(path=None):
808 """Set the path value in the global variable "repodir".
809 This makes bup look for an existing bup repository, but not fail if a
810 repository doesn't exist. Usually, if you are interacting with a bup
811 repository, you would not be calling this function but using
818 repodir = os.environ.get('BUP_DIR')
820 repodir = os.path.expanduser('~/.bup')
823 def init_repo(path=None):
824 """Create the Git bare repository for bup in a given path."""
826 d = repo() # appends a / to the path
827 parent = os.path.dirname(os.path.dirname(d))
828 if parent and not os.path.exists(parent):
829 raise GitError('parent directory "%s" does not exist\n' % parent)
830 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
831 raise GitError('"%s" exists but is not a directory\n' % d)
832 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
833 preexec_fn = _gitenv)
834 _git_wait('git init', p)
835 # Force the index version configuration in order to ensure bup works
836 # regardless of the version of the installed Git binary.
837 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
838 stdout=sys.stderr, preexec_fn = _gitenv)
839 _git_wait('git config', p)
841 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
842 stdout=sys.stderr, preexec_fn = _gitenv)
843 _git_wait('git config', p)
846 def check_repo_or_die(path=None):
847 """Make sure a bup repository exists, and abort if not.
848 If the path to a particular repository was not specified, this function
849 initializes the default repository automatically.
853 os.stat(repo('objects/pack/.'))
855 if e.errno == errno.ENOENT:
856 log('error: %r is not a bup repository; run "bup init"\n'
860 log('error: %s\n' % e)
866 """Get Git's version and ensure a usable version is installed.
868 The returned version is formatted as an ordered tuple with each position
869 representing a digit in the version tag. For example, the following tuple
870 would represent version 1.6.6.9:
876 p = subprocess.Popen(['git', '--version'],
877 stdout=subprocess.PIPE)
878 gvs = p.stdout.read()
879 _git_wait('git --version', p)
880 m = re.match(r'git version (\S+.\S+)', gvs)
882 raise GitError('git --version weird output: %r' % gvs)
883 _ver = tuple(m.group(1).split('.'))
884 needed = ('1','5', '3', '1')
886 raise GitError('git version %s or higher is required; you have %s'
887 % ('.'.join(needed), '.'.join(_ver)))
891 def _git_wait(cmd, p):
894 raise GitError('%s returned %d' % (cmd, rv))
897 def _git_capture(argv):
898 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
900 _git_wait(repr(argv), p)
904 class _AbortableIter:
905 def __init__(self, it, onabort = None):
907 self.onabort = onabort
915 return self.it.next()
916 except StopIteration, e:
924 """Abort iteration and call the abortion callback, if needed."""
936 """Link to 'git cat-file' that is used to retrieve blob data."""
939 wanted = ('1','5','6')
942 log('warning: git version < %s; bup will be slow.\n'
945 self.get = self._slow_get
947 self.p = self.inprogress = None
948 self.get = self._fast_get
952 self.p.stdout.close()
955 self.inprogress = None
959 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
960 stdin=subprocess.PIPE,
961 stdout=subprocess.PIPE,
964 preexec_fn = _gitenv)
966 def _fast_get(self, id):
967 if not self.p or self.p.poll() != None:
970 poll_result = self.p.poll()
971 assert(poll_result == None)
973 log('_fast_get: opening %r while %r is open\n'
974 % (id, self.inprogress))
975 assert(not self.inprogress)
976 assert(id.find('\n') < 0)
977 assert(id.find('\r') < 0)
978 assert(not id.startswith('-'))
980 self.p.stdin.write('%s\n' % id)
982 hdr = self.p.stdout.readline()
983 if hdr.endswith(' missing\n'):
984 self.inprogress = None
985 raise KeyError('blob %r is missing' % id)
987 if len(spl) != 3 or len(spl[0]) != 40:
988 raise GitError('expected blob, got %r' % spl)
989 (hex, type, size) = spl
991 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
992 onabort = self._abort)
997 readline_result = self.p.stdout.readline()
998 assert(readline_result == '\n')
999 self.inprogress = None
1000 except Exception, e:
1004 def _slow_get(self, id):
1005 assert(id.find('\n') < 0)
1006 assert(id.find('\r') < 0)
1007 assert(id[0] != '-')
1008 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1011 p = subprocess.Popen(['git', 'cat-file', type, id],
1012 stdout=subprocess.PIPE,
1013 preexec_fn = _gitenv)
1014 for blob in chunkyreader(p.stdout):
1016 _git_wait('git cat-file', p)
1018 def _join(self, it):
1023 elif type == 'tree':
1024 treefile = ''.join(it)
1025 for (mode, name, sha) in tree_decode(treefile):
1026 for blob in self.join(sha.encode('hex')):
1028 elif type == 'commit':
1029 treeline = ''.join(it).split('\n')[0]
1030 assert(treeline.startswith('tree '))
1031 for blob in self.join(treeline[5:]):
1034 raise GitError('invalid object type %r: expected blob/tree/commit'
1038 """Generate a list of the content of all blobs that can be reached
1039 from an object. The hash given in 'id' must point to a blob, a tree
1040 or a commit. The content of all blobs that can be seen from trees or
1041 commits will be added to the list.
1044 for d in self._join(self.get(id)):
1046 except StopIteration:
1050 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1052 for (n,c) in list_refs():
1053 if n.startswith('refs/tags/'):
1058 tags[c].append(name) # more than one tag can point at 'c'