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
14 home_repodir = os.path.expanduser('~/.bup')
17 _typemap = { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
18 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
24 class GitError(Exception):
29 """Get the path to the git repository or one of its subdirectories."""
32 raise GitError('You should call check_repo_or_die()')
34 # If there's a .git subdirectory, then the actual repo is in there.
35 gd = os.path.join(repodir, '.git')
36 if os.path.exists(gd):
39 return os.path.join(repodir, sub)
43 return re.sub(r'([^0-9a-z]|\b)([0-9a-z]{7})[0-9a-z]{33}([^0-9a-z]|\b)',
48 full = os.path.abspath(path)
49 fullrepo = os.path.abspath(repo(''))
50 if not fullrepo.endswith('/'):
52 if full.startswith(fullrepo):
53 path = full[len(fullrepo):]
54 if path.startswith('index-cache/'):
55 path = path[len('index-cache/'):]
56 return shorten_hash(path)
60 paths = [repo('objects/pack')]
61 paths += glob.glob(repo('index-cache/*/.'))
65 def auto_midx(objdir):
66 args = [path.exe(), 'midx', '--auto', '--dir', objdir]
68 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
70 # make sure 'args' gets printed to help with debugging
71 add_error('%r: exception: %s' % (args, e))
74 add_error('%r: returned %d' % (args, rv))
76 args = [path.exe(), 'bloom', '--dir', objdir]
78 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
80 # make sure 'args' gets printed to help with debugging
81 add_error('%r: exception: %s' % (args, e))
84 add_error('%r: returned %d' % (args, rv))
87 def mangle_name(name, mode, gitmode):
88 """Mangle a file name to present an abstract name for segmented files.
89 Mangled file names will have the ".bup" extension added to them. If a
90 file's name already ends with ".bup", a ".bupl" extension is added to
91 disambiguate normal files from semgmented ones.
93 if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
95 elif name.endswith('.bup') or name[:-1].endswith('.bup'):
101 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
102 def demangle_name(name):
103 """Remove name mangling from a file name, if necessary.
105 The return value is a tuple (demangled_filename,mode), where mode is one of
108 * BUP_NORMAL : files that should be read as-is from the repository
109 * BUP_CHUNKED : files that were chunked and need to be assembled
111 For more information on the name mangling algorythm, see mangle_name()
113 if name.endswith('.bupl'):
114 return (name[:-5], BUP_NORMAL)
115 elif name.endswith('.bup'):
116 return (name[:-4], BUP_CHUNKED)
118 return (name, BUP_NORMAL)
121 def calc_hash(type, content):
122 """Calculate some content's hash in the Git fashion."""
123 header = '%s %d\0' % (type, len(content))
129 def shalist_item_sort_key(ent):
130 (mode, name, id) = ent
131 assert(mode+0 == mode)
132 if stat.S_ISDIR(mode):
138 def tree_encode(shalist):
139 """Generate a git tree object from (mode,name,hash) tuples."""
140 shalist = sorted(shalist, key = shalist_item_sort_key)
142 for (mode,name,bin) in shalist:
144 assert(mode+0 == mode)
146 assert(len(bin) == 20)
147 s = '%o %s\0%s' % (mode,name,bin)
148 assert(s[0] != '0') # 0-padded octal is not acceptable in a git tree
153 def tree_decode(buf):
154 """Generate a list of (mode,name,hash) from the git tree object in buf."""
156 while ofs < len(buf):
157 z = buf.find('\0', ofs)
159 spl = buf[ofs:z].split(' ', 1)
160 assert(len(spl) == 2)
162 sha = buf[z+1:z+1+20]
164 yield (int(mode, 8), name, sha)
167 def _encode_packobj(type, content, compression_level=1):
170 szbits = (sz & 0x0f) | (_typemap[type]<<4)
173 if sz: szbits |= 0x80
179 if compression_level > 9:
180 compression_level = 9
181 elif compression_level < 0:
182 compression_level = 0
183 z = zlib.compressobj(compression_level)
185 yield z.compress(content)
189 def _encode_looseobj(type, content, compression_level=1):
190 z = zlib.compressobj(compression_level)
191 yield z.compress('%s %d\0' % (type, len(content)))
192 yield z.compress(content)
196 def _decode_looseobj(buf):
198 s = zlib.decompress(buf)
205 assert(type in _typemap)
206 assert(sz == len(content))
207 return (type, content)
210 def _decode_packobj(buf):
213 type = _typermap[(c & 0x70) >> 4]
220 sz |= (c & 0x7f) << shift
224 return (type, zlib.decompress(buf[i+1:]))
231 def find_offset(self, hash):
232 """Get the offset of an object inside the index file."""
233 idx = self._idx_from_hash(hash)
235 return self._ofs_from_idx(idx)
238 def exists(self, hash, want_source=False):
239 """Return nonempty if the object exists in this index."""
240 if hash and (self._idx_from_hash(hash) != None):
241 return want_source and os.path.basename(self.name) or True
245 return int(self.fanout[255])
247 def _idx_from_hash(self, hash):
248 global _total_searches, _total_steps
250 assert(len(hash) == 20)
252 start = self.fanout[b1-1] # range -1..254
253 end = self.fanout[b1] # range 0..255
255 _total_steps += 1 # lookup table is a step
258 mid = start + (end-start)/2
259 v = self._idx_to_hash(mid)
269 class PackIdxV1(PackIdx):
270 """Object representation of a Git pack index (version 1) file."""
271 def __init__(self, filename, f):
273 self.idxnames = [self.name]
274 self.map = mmap_read(f)
275 self.fanout = list(struct.unpack('!256I',
276 str(buffer(self.map, 0, 256*4))))
277 self.fanout.append(0) # entry "-1"
278 nsha = self.fanout[255]
280 self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
282 def _ofs_from_idx(self, idx):
283 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
285 def _idx_to_hash(self, idx):
286 return str(self.shatable[idx*24+4 : idx*24+24])
289 for i in xrange(self.fanout[255]):
290 yield buffer(self.map, 256*4 + 24*i + 4, 20)
293 class PackIdxV2(PackIdx):
294 """Object representation of a Git pack index (version 2) file."""
295 def __init__(self, filename, f):
297 self.idxnames = [self.name]
298 self.map = mmap_read(f)
299 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
300 self.fanout = list(struct.unpack('!256I',
301 str(buffer(self.map, 8, 256*4))))
302 self.fanout.append(0) # entry "-1"
303 nsha = self.fanout[255]
304 self.sha_ofs = 8 + 256*4
305 self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
306 self.ofstable = buffer(self.map,
307 self.sha_ofs + nsha*20 + nsha*4,
309 self.ofs64table = buffer(self.map,
310 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
312 def _ofs_from_idx(self, idx):
313 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
315 idx64 = ofs & 0x7fffffff
316 ofs = struct.unpack('!Q',
317 str(buffer(self.ofs64table, idx64*8, 8)))[0]
320 def _idx_to_hash(self, idx):
321 return str(self.shatable[idx*20:(idx+1)*20])
324 for i in xrange(self.fanout[255]):
325 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
330 def __init__(self, dir):
332 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
337 self.do_bloom = False
344 assert(_mpi_count == 0)
347 return iter(idxmerge(self.packs))
350 return sum(len(pack) for pack in self.packs)
352 def exists(self, hash, want_source=False):
353 """Return nonempty if the object exists in the index files."""
354 global _total_searches
356 if hash in self.also:
358 if self.do_bloom and self.bloom:
359 if self.bloom.exists(hash):
360 self.do_bloom = False
362 _total_searches -= 1 # was counted by bloom
364 for i in xrange(len(self.packs)):
366 _total_searches -= 1 # will be incremented by sub-pack
367 ix = p.exists(hash, want_source=want_source)
369 # reorder so most recently used packs are searched first
370 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
375 def refresh(self, skip_midx = False):
376 """Refresh the index list.
377 This method verifies if .midx files were superseded (e.g. all of its
378 contents are in another, bigger .midx file) and removes the superseded
381 If skip_midx is True, all work on .midx files will be skipped and .midx
382 files will be removed from the list.
384 The module-global variable 'ignore_midx' can force this function to
385 always act as if skip_midx was True.
387 self.bloom = None # Always reopen the bloom as it may have been relaced
388 self.do_bloom = False
389 skip_midx = skip_midx or ignore_midx
390 d = dict((p.name, p) for p in self.packs
391 if not skip_midx or not isinstance(p, midx.PackMidx))
392 if os.path.exists(self.dir):
395 for ix in self.packs:
396 if isinstance(ix, midx.PackMidx):
397 for name in ix.idxnames:
398 d[os.path.join(self.dir, name)] = ix
399 for full in glob.glob(os.path.join(self.dir,'*.midx')):
401 mx = midx.PackMidx(full)
402 (mxd, mxf) = os.path.split(mx.name)
404 for n in mx.idxnames:
405 if not os.path.exists(os.path.join(mxd, n)):
406 log(('warning: index %s missing\n' +
407 ' used by %s\n') % (n, mxf))
414 midxl.sort(key=lambda ix:
415 (-len(ix), -xstat.stat(ix.name).st_mtime))
418 for sub in ix.idxnames:
419 found = d.get(os.path.join(self.dir, sub))
420 if not found or isinstance(found, PackIdx):
421 # doesn't exist, or exists but not in a midx
426 for name in ix.idxnames:
427 d[os.path.join(self.dir, name)] = ix
428 elif not ix.force_keep:
429 debug1('midx: removing redundant: %s\n'
430 % os.path.basename(ix.name))
432 for full in glob.glob(os.path.join(self.dir,'*.idx')):
440 bfull = os.path.join(self.dir, 'bup.bloom')
441 if self.bloom is None and os.path.exists(bfull):
442 self.bloom = bloom.ShaBloom(bfull)
443 self.packs = list(set(d.values()))
444 self.packs.sort(lambda x,y: -cmp(len(x),len(y)))
445 if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
449 debug1('PackIdxList: using %d index%s.\n'
450 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
453 """Insert an additional object in the list."""
457 def open_idx(filename):
458 if filename.endswith('.idx'):
459 f = open(filename, 'rb')
461 if header[0:4] == '\377tOc':
462 version = struct.unpack('!I', header[4:8])[0]
464 return PackIdxV2(filename, f)
466 raise GitError('%s: expected idx file version 2, got %d'
467 % (filename, version))
468 elif len(header) == 8 and header[0:4] < '\377tOc':
469 return PackIdxV1(filename, f)
471 raise GitError('%s: unrecognized idx file header' % filename)
472 elif filename.endswith('.midx'):
473 return midx.PackMidx(filename)
475 raise GitError('idx filenames must end with .idx or .midx')
478 def idxmerge(idxlist, final_progress=True):
479 """Generate a list of all the objects reachable in a PackIdxList."""
480 def pfunc(count, total):
481 qprogress('Reading indexes: %.2f%% (%d/%d)\r'
482 % (count*100.0/total, count, total))
483 def pfinal(count, total):
485 progress('Reading indexes: %.2f%% (%d/%d), done.\n'
486 % (100, total, total))
487 return merge_iter(idxlist, 10024, pfunc, pfinal)
490 def _make_objcache():
491 return PackIdxList(repo('objects/pack'))
494 """Writes Git objects inside a pack file."""
495 def __init__(self, objcache_maker=_make_objcache, compression_level=1):
501 self.objcache_maker = objcache_maker
503 self.compression_level = compression_level
510 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
511 self.file = os.fdopen(fd, 'w+b')
512 assert(name.endswith('.pack'))
513 self.filename = name[:-5]
514 self.file.write('PACK\0\0\0\2\0\0\0\0')
515 self.idx = list(list() for i in xrange(256))
517 def _raw_write(self, datalist, sha):
520 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
521 # the file never has a *partial* blob. So let's make sure it's
522 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
523 # to our hashsplit algorithm.) f.write() does its own buffering,
524 # but that's okay because we'll flush it in _end().
525 oneblob = ''.join(datalist)
529 raise GitError, e, sys.exc_info()[2]
531 crc = zlib.crc32(oneblob) & 0xffffffff
532 self._update_idx(sha, crc, nw)
537 def _update_idx(self, sha, crc, size):
540 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
542 def _write(self, sha, type, content):
546 sha = calc_hash(type, content)
547 size, crc = self._raw_write(_encode_packobj(type, content,
548 self.compression_level),
550 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
554 def breakpoint(self):
555 """Clear byte and object counts and return the last processed id."""
557 self.outbytes = self.count = 0
560 def _require_objcache(self):
561 if self.objcache is None and self.objcache_maker:
562 self.objcache = self.objcache_maker()
563 if self.objcache is None:
565 "PackWriter not opened or can't check exists w/o objcache")
567 def exists(self, id, want_source=False):
568 """Return non-empty if an object is found in the object cache."""
569 self._require_objcache()
570 return self.objcache.exists(id, want_source=want_source)
572 def maybe_write(self, type, content):
573 """Write an object to the pack file if not present and return its id."""
574 sha = calc_hash(type, content)
575 if not self.exists(sha):
576 self._write(sha, type, content)
577 self._require_objcache()
578 self.objcache.add(sha)
581 def new_blob(self, blob):
582 """Create a blob object in the pack with the supplied content."""
583 return self.maybe_write('blob', blob)
585 def new_tree(self, shalist):
586 """Create a tree object in the pack."""
587 content = tree_encode(shalist)
588 return self.maybe_write('tree', content)
590 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
592 if tree: l.append('tree %s' % tree.encode('hex'))
593 if parent: l.append('parent %s' % parent.encode('hex'))
594 if author: l.append('author %s %s' % (author, _git_date(adate)))
595 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
598 return self.maybe_write('commit', '\n'.join(l))
600 def new_commit(self, parent, tree, date, msg):
601 """Create a commit object in the pack."""
602 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
603 commit = self._new_commit(tree, parent,
604 userline, date, userline, date,
609 """Remove the pack file from disk."""
615 os.unlink(self.filename + '.pack')
617 def _end(self, run_midx=True):
619 if not f: return None
625 # update object count
627 cp = struct.pack('!i', self.count)
631 # calculate the pack sha1sum
634 for b in chunkyreader(f):
636 packbin = sum.digest()
640 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
642 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
643 if os.path.exists(self.filename + '.map'):
644 os.unlink(self.filename + '.map')
645 os.rename(self.filename + '.pack', nameprefix + '.pack')
646 os.rename(self.filename + '.idx', nameprefix + '.idx')
649 auto_midx(repo('objects/pack'))
652 def close(self, run_midx=True):
653 """Close the pack file and move it to its definitive path."""
654 return self._end(run_midx=run_midx)
656 def _write_pack_idx_v2(self, filename, idx, packbin):
659 for entry in section:
660 if entry[2] >= 2**31:
663 # Length: header + fan-out + shas-and-crcs + overflow-offsets
664 index_len = 8 + (4 * 256) + (28 * self.count) + (8 * ofs64_count)
666 idx_f = open(filename, 'w+b')
668 idx_f.truncate(index_len)
669 idx_map = mmap_readwrite(idx_f, close=False)
670 count = _helpers.write_idx(filename, idx_map, idx, self.count)
671 assert(count == self.count)
673 if idx_map: idx_map.close()
676 idx_f = open(filename, 'a+b')
681 b = idx_f.read(8 + 4*256)
684 obj_list_sum = Sha1()
685 for b in chunkyreader(idx_f, 20*self.count):
687 obj_list_sum.update(b)
688 namebase = obj_list_sum.hexdigest()
690 for b in chunkyreader(idx_f):
692 idx_f.write(idx_sum.digest())
699 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
703 os.environ['GIT_DIR'] = os.path.abspath(repo())
706 def list_refs(refname = None):
707 """Generate a list of tuples in the form (refname,hash).
708 If a ref name is specified, list only this particular ref.
710 argv = ['git', 'show-ref', '--']
713 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
714 out = p.stdout.read().strip()
715 rv = p.wait() # not fatal
719 for d in out.split('\n'):
720 (sha, name) = d.split(' ', 1)
721 yield (name, sha.decode('hex'))
724 def read_ref(refname):
725 """Get the commit id of the most recent commit made on a given ref."""
726 l = list(list_refs(refname))
734 def rev_list(ref, count=None):
735 """Generate a list of reachable commits in reverse chronological order.
737 This generator walks through commits, from child to parent, that are
738 reachable via the specified ref and yields a series of tuples of the form
741 If count is a non-zero integer, limit the number of commits to "count"
744 assert(not ref.startswith('-'))
747 opts += ['-n', str(atoi(count))]
748 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
749 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
753 if s.startswith('commit '):
754 commit = s[7:].decode('hex')
758 rv = p.wait() # not fatal
760 raise GitError, 'git rev-list returned error %d' % rv
763 def rev_get_date(ref):
764 """Get the date of the latest commit on the specified ref."""
765 for (date, commit) in rev_list(ref, count=1):
767 raise GitError, 'no such commit %r' % ref
770 def rev_parse(committish):
771 """Resolve the full hash for 'committish', if it exists.
773 Should be roughly equivalent to 'git rev-parse'.
775 Returns the hex value of the hash if it is found, None if 'committish' does
776 not correspond to anything.
778 head = read_ref(committish)
780 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
783 pL = PackIdxList(repo('objects/pack'))
785 if len(committish) == 40:
787 hash = committish.decode('hex')
797 def update_ref(refname, newval, oldval):
798 """Change the commit pointed to by a branch."""
801 assert(refname.startswith('refs/heads/'))
802 p = subprocess.Popen(['git', 'update-ref', refname,
803 newval.encode('hex'), oldval.encode('hex')],
804 preexec_fn = _gitenv)
805 _git_wait('git update-ref', p)
808 def guess_repo(path=None):
809 """Set the path value in the global variable "repodir".
810 This makes bup look for an existing bup repository, but not fail if a
811 repository doesn't exist. Usually, if you are interacting with a bup
812 repository, you would not be calling this function but using
819 repodir = os.environ.get('BUP_DIR')
821 repodir = os.path.expanduser('~/.bup')
824 def init_repo(path=None):
825 """Create the Git bare repository for bup in a given path."""
827 d = repo() # appends a / to the path
828 parent = os.path.dirname(os.path.dirname(d))
829 if parent and not os.path.exists(parent):
830 raise GitError('parent directory "%s" does not exist\n' % parent)
831 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
832 raise GitError('"%s" exists but is not a directory\n' % d)
833 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
834 preexec_fn = _gitenv)
835 _git_wait('git init', p)
836 # Force the index version configuration in order to ensure bup works
837 # regardless of the version of the installed Git binary.
838 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
839 stdout=sys.stderr, preexec_fn = _gitenv)
840 _git_wait('git config', p)
842 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
843 stdout=sys.stderr, preexec_fn = _gitenv)
844 _git_wait('git config', p)
847 def check_repo_or_die(path=None):
848 """Make sure a bup repository exists, and abort if not.
849 If the path to a particular repository was not specified, this function
850 initializes the default repository automatically.
854 os.stat(repo('objects/pack/.'))
856 if e.errno == errno.ENOENT:
857 log('error: %r is not a bup repository; run "bup init"\n'
861 log('error: %s\n' % e)
867 """Get Git's version and ensure a usable version is installed.
869 The returned version is formatted as an ordered tuple with each position
870 representing a digit in the version tag. For example, the following tuple
871 would represent version 1.6.6.9:
877 p = subprocess.Popen(['git', '--version'],
878 stdout=subprocess.PIPE)
879 gvs = p.stdout.read()
880 _git_wait('git --version', p)
881 m = re.match(r'git version (\S+.\S+)', gvs)
883 raise GitError('git --version weird output: %r' % gvs)
884 _ver = tuple(m.group(1).split('.'))
885 needed = ('1','5', '3', '1')
887 raise GitError('git version %s or higher is required; you have %s'
888 % ('.'.join(needed), '.'.join(_ver)))
892 def _git_wait(cmd, p):
895 raise GitError('%s returned %d' % (cmd, rv))
898 def _git_capture(argv):
899 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
901 _git_wait(repr(argv), p)
905 class _AbortableIter:
906 def __init__(self, it, onabort = None):
908 self.onabort = onabort
916 return self.it.next()
917 except StopIteration, e:
925 """Abort iteration and call the abortion callback, if needed."""
937 """Link to 'git cat-file' that is used to retrieve blob data."""
940 wanted = ('1','5','6')
943 log('warning: git version < %s; bup will be slow.\n'
946 self.get = self._slow_get
948 self.p = self.inprogress = None
949 self.get = self._fast_get
953 self.p.stdout.close()
956 self.inprogress = None
960 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
961 stdin=subprocess.PIPE,
962 stdout=subprocess.PIPE,
965 preexec_fn = _gitenv)
967 def _fast_get(self, id):
968 if not self.p or self.p.poll() != None:
971 poll_result = self.p.poll()
972 assert(poll_result == None)
974 log('_fast_get: opening %r while %r is open\n'
975 % (id, self.inprogress))
976 assert(not self.inprogress)
977 assert(id.find('\n') < 0)
978 assert(id.find('\r') < 0)
979 assert(not id.startswith('-'))
981 self.p.stdin.write('%s\n' % id)
983 hdr = self.p.stdout.readline()
984 if hdr.endswith(' missing\n'):
985 self.inprogress = None
986 raise KeyError('blob %r is missing' % id)
988 if len(spl) != 3 or len(spl[0]) != 40:
989 raise GitError('expected blob, got %r' % spl)
990 (hex, type, size) = spl
992 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
993 onabort = self._abort)
998 readline_result = self.p.stdout.readline()
999 assert(readline_result == '\n')
1000 self.inprogress = None
1001 except Exception, e:
1005 def _slow_get(self, id):
1006 assert(id.find('\n') < 0)
1007 assert(id.find('\r') < 0)
1008 assert(id[0] != '-')
1009 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1012 p = subprocess.Popen(['git', 'cat-file', type, id],
1013 stdout=subprocess.PIPE,
1014 preexec_fn = _gitenv)
1015 for blob in chunkyreader(p.stdout):
1017 _git_wait('git cat-file', p)
1019 def _join(self, it):
1024 elif type == 'tree':
1025 treefile = ''.join(it)
1026 for (mode, name, sha) in tree_decode(treefile):
1027 for blob in self.join(sha.encode('hex')):
1029 elif type == 'commit':
1030 treeline = ''.join(it).split('\n')[0]
1031 assert(treeline.startswith('tree '))
1032 for blob in self.join(treeline[5:]):
1035 raise GitError('invalid object type %r: expected blob/tree/commit'
1039 """Generate a list of the content of all blobs that can be reached
1040 from an object. The hash given in 'id' must point to a blob, a tree
1041 or a commit. The content of all blobs that can be seen from trees or
1042 commits will be added to the list.
1045 for d in self._join(self.get(id)):
1047 except StopIteration:
1051 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1053 for (n,c) in list_refs():
1054 if n.startswith('refs/tags/'):
1059 tags[c].append(name) # more than one tag can point at 'c'