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
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_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_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[ofs:].find('\0')
160 spl = buf[ofs:ofs+z].split(' ', 1)
161 assert(len(spl) == 2)
163 sha = buf[ofs+z+1:ofs+z+1+20]
165 yield (int(mode, 8), name, sha)
168 def _encode_packobj(type, content):
171 szbits = (sz & 0x0f) | (_typemap[type]<<4)
174 if sz: szbits |= 0x80
180 z = zlib.compressobj(1)
182 yield z.compress(content)
186 def _encode_looseobj(type, content):
187 z = zlib.compressobj(1)
188 yield z.compress('%s %d\0' % (type, len(content)))
189 yield z.compress(content)
193 def _decode_looseobj(buf):
195 s = zlib.decompress(buf)
202 assert(type in _typemap)
203 assert(sz == len(content))
204 return (type, content)
207 def _decode_packobj(buf):
210 type = _typermap[(c & 0x70) >> 4]
217 sz |= (c & 0x7f) << shift
221 return (type, zlib.decompress(buf[i+1:]))
228 def find_offset(self, hash):
229 """Get the offset of an object inside the index file."""
230 idx = self._idx_from_hash(hash)
232 return self._ofs_from_idx(idx)
235 def exists(self, hash, want_source=False):
236 """Return nonempty if the object exists in this index."""
237 if hash and (self._idx_from_hash(hash) != None):
238 return want_source and os.path.basename(self.name) or True
242 return int(self.fanout[255])
244 def _idx_from_hash(self, hash):
245 global _total_searches, _total_steps
247 assert(len(hash) == 20)
249 start = self.fanout[b1-1] # range -1..254
250 end = self.fanout[b1] # range 0..255
252 _total_steps += 1 # lookup table is a step
255 mid = start + (end-start)/2
256 v = self._idx_to_hash(mid)
266 class PackIdxV1(PackIdx):
267 """Object representation of a Git pack index (version 1) file."""
268 def __init__(self, filename, f):
270 self.idxnames = [self.name]
271 self.map = mmap_read(f)
272 self.fanout = list(struct.unpack('!256I',
273 str(buffer(self.map, 0, 256*4))))
274 self.fanout.append(0) # entry "-1"
275 nsha = self.fanout[255]
277 self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
279 def _ofs_from_idx(self, idx):
280 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
282 def _idx_to_hash(self, idx):
283 return str(self.shatable[idx*24+4 : idx*24+24])
286 for i in xrange(self.fanout[255]):
287 yield buffer(self.map, 256*4 + 24*i + 4, 20)
290 class PackIdxV2(PackIdx):
291 """Object representation of a Git pack index (version 2) file."""
292 def __init__(self, filename, f):
294 self.idxnames = [self.name]
295 self.map = mmap_read(f)
296 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
297 self.fanout = list(struct.unpack('!256I',
298 str(buffer(self.map, 8, 256*4))))
299 self.fanout.append(0) # entry "-1"
300 nsha = self.fanout[255]
301 self.sha_ofs = 8 + 256*4
302 self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
303 self.ofstable = buffer(self.map,
304 self.sha_ofs + nsha*20 + nsha*4,
306 self.ofs64table = buffer(self.map,
307 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
309 def _ofs_from_idx(self, idx):
310 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
312 idx64 = ofs & 0x7fffffff
313 ofs = struct.unpack('!Q',
314 str(buffer(self.ofs64table, idx64*8, 8)))[0]
317 def _idx_to_hash(self, idx):
318 return str(self.shatable[idx*20:(idx+1)*20])
321 for i in xrange(self.fanout[255]):
322 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
327 def __init__(self, dir):
329 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
334 self.do_bloom = False
341 assert(_mpi_count == 0)
344 return iter(idxmerge(self.packs))
347 return sum(len(pack) for pack in self.packs)
349 def exists(self, hash, want_source=False):
350 """Return nonempty if the object exists in the index files."""
351 global _total_searches
353 if hash in self.also:
355 if self.do_bloom and self.bloom:
356 if self.bloom.exists(hash):
357 self.do_bloom = False
359 _total_searches -= 1 # was counted by bloom
361 for i in xrange(len(self.packs)):
363 _total_searches -= 1 # will be incremented by sub-pack
364 ix = p.exists(hash, want_source=want_source)
366 # reorder so most recently used packs are searched first
367 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
372 def refresh(self, skip_midx = False):
373 """Refresh the index list.
374 This method verifies if .midx files were superseded (e.g. all of its
375 contents are in another, bigger .midx file) and removes the superseded
378 If skip_midx is True, all work on .midx files will be skipped and .midx
379 files will be removed from the list.
381 The module-global variable 'ignore_midx' can force this function to
382 always act as if skip_midx was True.
384 self.bloom = None # Always reopen the bloom as it may have been relaced
385 self.do_bloom = False
386 skip_midx = skip_midx or ignore_midx
387 d = dict((p.name, p) for p in self.packs
388 if not skip_midx or not isinstance(p, midx.PackMidx))
389 if os.path.exists(self.dir):
392 for ix in self.packs:
393 if isinstance(ix, midx.PackMidx):
394 for name in ix.idxnames:
395 d[os.path.join(self.dir, name)] = ix
396 for full in glob.glob(os.path.join(self.dir,'*.midx')):
398 mx = midx.PackMidx(full)
399 (mxd, mxf) = os.path.split(mx.name)
401 for n in mx.idxnames:
402 if not os.path.exists(os.path.join(mxd, n)):
403 log(('warning: index %s missing\n' +
404 ' used by %s\n') % (n, mxf))
411 midxl.sort(key=lambda ix:
412 (-len(ix), -os.stat(ix.name).st_mtime))
415 for sub in ix.idxnames:
416 found = d.get(os.path.join(self.dir, sub))
417 if not found or isinstance(found, PackIdx):
418 # doesn't exist, or exists but not in a midx
423 for name in ix.idxnames:
424 d[os.path.join(self.dir, name)] = ix
425 elif not ix.force_keep:
426 debug1('midx: removing redundant: %s\n'
427 % os.path.basename(ix.name))
429 for full in glob.glob(os.path.join(self.dir,'*.idx')):
437 bfull = os.path.join(self.dir, 'bup.bloom')
438 if self.bloom is None and os.path.exists(bfull):
439 self.bloom = bloom.ShaBloom(bfull)
440 self.packs = list(set(d.values()))
441 self.packs.sort(lambda x,y: -cmp(len(x),len(y)))
442 if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
446 debug1('PackIdxList: using %d index%s.\n'
447 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
450 """Insert an additional object in the list."""
454 def open_idx(filename):
455 if filename.endswith('.idx'):
456 f = open(filename, 'rb')
458 if header[0:4] == '\377tOc':
459 version = struct.unpack('!I', header[4:8])[0]
461 return PackIdxV2(filename, f)
463 raise GitError('%s: expected idx file version 2, got %d'
464 % (filename, version))
465 elif len(header) == 8 and header[0:4] < '\377tOc':
466 return PackIdxV1(filename, f)
468 raise GitError('%s: unrecognized idx file header' % filename)
469 elif filename.endswith('.midx'):
470 return midx.PackMidx(filename)
472 raise GitError('idx filenames must end with .idx or .midx')
475 def idxmerge(idxlist, final_progress=True):
476 """Generate a list of all the objects reachable in a PackIdxList."""
477 def pfunc(count, total):
478 qprogress('Reading indexes: %.2f%% (%d/%d)\r'
479 % (count*100.0/total, count, total))
480 def pfinal(count, total):
482 progress('Reading indexes: %.2f%% (%d/%d), done.\n'
483 % (100, total, total))
484 return merge_iter(idxlist, 10024, pfunc, pfinal)
487 def _make_objcache():
488 return PackIdxList(repo('objects/pack'))
491 """Writes Git objects inside a pack file."""
492 def __init__(self, objcache_maker=_make_objcache):
498 self.objcache_maker = objcache_maker
506 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
507 self.file = os.fdopen(fd, 'w+b')
508 assert(name.endswith('.pack'))
509 self.filename = name[:-5]
510 self.file.write('PACK\0\0\0\2\0\0\0\0')
511 self.idx = list(list() for i in xrange(256))
513 def _raw_write(self, datalist, sha):
516 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
517 # the file never has a *partial* blob. So let's make sure it's
518 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
519 # to our hashsplit algorithm.) f.write() does its own buffering,
520 # but that's okay because we'll flush it in _end().
521 oneblob = ''.join(datalist)
525 raise GitError, e, sys.exc_info()[2]
527 crc = zlib.crc32(oneblob) & 0xffffffff
528 self._update_idx(sha, crc, nw)
533 def _update_idx(self, sha, crc, size):
536 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
538 def _write(self, sha, type, content):
542 sha = calc_hash(type, content)
543 size, crc = self._raw_write(_encode_packobj(type, content), sha=sha)
544 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
548 def breakpoint(self):
549 """Clear byte and object counts and return the last processed id."""
551 self.outbytes = self.count = 0
554 def _require_objcache(self):
555 if self.objcache is None and self.objcache_maker:
556 self.objcache = self.objcache_maker()
557 if self.objcache is None:
559 "PackWriter not opened or can't check exists w/o objcache")
561 def exists(self, id, want_source=False):
562 """Return non-empty if an object is found in the object cache."""
563 self._require_objcache()
564 return self.objcache.exists(id, want_source=want_source)
566 def maybe_write(self, type, content):
567 """Write an object to the pack file if not present and return its id."""
568 sha = calc_hash(type, content)
569 if not self.exists(sha):
570 self._write(sha, type, content)
571 self._require_objcache()
572 self.objcache.add(sha)
575 def new_blob(self, blob):
576 """Create a blob object in the pack with the supplied content."""
577 return self.maybe_write('blob', blob)
579 def new_tree(self, shalist):
580 """Create a tree object in the pack."""
581 content = tree_encode(shalist)
582 return self.maybe_write('tree', content)
584 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
586 if tree: l.append('tree %s' % tree.encode('hex'))
587 if parent: l.append('parent %s' % parent.encode('hex'))
588 if author: l.append('author %s %s' % (author, _git_date(adate)))
589 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
592 return self.maybe_write('commit', '\n'.join(l))
594 def new_commit(self, parent, tree, date, msg):
595 """Create a commit object in the pack."""
596 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
597 commit = self._new_commit(tree, parent,
598 userline, date, userline, date,
603 """Remove the pack file from disk."""
609 os.unlink(self.filename + '.pack')
611 def _end(self, run_midx=True):
613 if not f: return None
619 # update object count
621 cp = struct.pack('!i', self.count)
625 # calculate the pack sha1sum
628 for b in chunkyreader(f):
630 packbin = sum.digest()
634 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
636 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
637 if os.path.exists(self.filename + '.map'):
638 os.unlink(self.filename + '.map')
639 os.rename(self.filename + '.pack', nameprefix + '.pack')
640 os.rename(self.filename + '.idx', nameprefix + '.idx')
643 auto_midx(repo('objects/pack'))
646 def close(self, run_midx=True):
647 """Close the pack file and move it to its definitive path."""
648 return self._end(run_midx=run_midx)
650 def _write_pack_idx_v2(self, filename, idx, packbin):
651 idx_f = open(filename, 'w+b')
652 idx_f.write('\377tOc\0\0\0\2')
654 ofs64_ofs = 8 + 4*256 + 28*self.count
655 idx_f.truncate(ofs64_ofs)
657 idx_map = mmap_readwrite(idx_f, close=False)
658 idx_f.seek(0, SEEK_END)
659 count = _helpers.write_idx(idx_f, idx_map, idx, self.count)
660 assert(count == self.count)
666 b = idx_f.read(8 + 4*256)
669 obj_list_sum = Sha1()
670 for b in chunkyreader(idx_f, 20*self.count):
672 obj_list_sum.update(b)
673 namebase = obj_list_sum.hexdigest()
675 for b in chunkyreader(idx_f):
677 idx_f.write(idx_sum.digest())
684 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
688 os.environ['GIT_DIR'] = os.path.abspath(repo())
691 def list_refs(refname = None):
692 """Generate a list of tuples in the form (refname,hash).
693 If a ref name is specified, list only this particular ref.
695 argv = ['git', 'show-ref', '--']
698 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
699 out = p.stdout.read().strip()
700 rv = p.wait() # not fatal
704 for d in out.split('\n'):
705 (sha, name) = d.split(' ', 1)
706 yield (name, sha.decode('hex'))
709 def read_ref(refname):
710 """Get the commit id of the most recent commit made on a given ref."""
711 l = list(list_refs(refname))
719 def rev_list(ref, count=None):
720 """Generate a list of reachable commits in reverse chronological order.
722 This generator walks through commits, from child to parent, that are
723 reachable via the specified ref and yields a series of tuples of the form
726 If count is a non-zero integer, limit the number of commits to "count"
729 assert(not ref.startswith('-'))
732 opts += ['-n', str(atoi(count))]
733 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
734 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
738 if s.startswith('commit '):
739 commit = s[7:].decode('hex')
743 rv = p.wait() # not fatal
745 raise GitError, 'git rev-list returned error %d' % rv
748 def rev_get_date(ref):
749 """Get the date of the latest commit on the specified ref."""
750 for (date, commit) in rev_list(ref, count=1):
752 raise GitError, 'no such commit %r' % ref
755 def rev_parse(committish):
756 """Resolve the full hash for 'committish', if it exists.
758 Should be roughly equivalent to 'git rev-parse'.
760 Returns the hex value of the hash if it is found, None if 'committish' does
761 not correspond to anything.
763 head = read_ref(committish)
765 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
768 pL = PackIdxList(repo('objects/pack'))
770 if len(committish) == 40:
772 hash = committish.decode('hex')
782 def update_ref(refname, newval, oldval):
783 """Change the commit pointed to by a branch."""
786 assert(refname.startswith('refs/heads/'))
787 p = subprocess.Popen(['git', 'update-ref', refname,
788 newval.encode('hex'), oldval.encode('hex')],
789 preexec_fn = _gitenv)
790 _git_wait('git update-ref', p)
793 def guess_repo(path=None):
794 """Set the path value in the global variable "repodir".
795 This makes bup look for an existing bup repository, but not fail if a
796 repository doesn't exist. Usually, if you are interacting with a bup
797 repository, you would not be calling this function but using
804 repodir = os.environ.get('BUP_DIR')
806 repodir = os.path.expanduser('~/.bup')
809 def init_repo(path=None):
810 """Create the Git bare repository for bup in a given path."""
812 d = repo() # appends a / to the path
813 parent = os.path.dirname(os.path.dirname(d))
814 if parent and not os.path.exists(parent):
815 raise GitError('parent directory "%s" does not exist\n' % parent)
816 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
817 raise GitError('"%d" exists but is not a directory\n' % d)
818 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
819 preexec_fn = _gitenv)
820 _git_wait('git init', p)
821 # Force the index version configuration in order to ensure bup works
822 # regardless of the version of the installed Git binary.
823 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
824 stdout=sys.stderr, preexec_fn = _gitenv)
825 _git_wait('git config', p)
828 def check_repo_or_die(path=None):
829 """Make sure a bup repository exists, and abort if not.
830 If the path to a particular repository was not specified, this function
831 initializes the default repository automatically.
834 if not os.path.isdir(repo('objects/pack/.')):
835 if repodir == home_repodir:
838 log('error: %r is not a bup/git repository\n' % repo())
844 """Get Git's version and ensure a usable version is installed.
846 The returned version is formatted as an ordered tuple with each position
847 representing a digit in the version tag. For example, the following tuple
848 would represent version 1.6.6.9:
854 p = subprocess.Popen(['git', '--version'],
855 stdout=subprocess.PIPE)
856 gvs = p.stdout.read()
857 _git_wait('git --version', p)
858 m = re.match(r'git version (\S+.\S+)', gvs)
860 raise GitError('git --version weird output: %r' % gvs)
861 _ver = tuple(m.group(1).split('.'))
862 needed = ('1','5', '3', '1')
864 raise GitError('git version %s or higher is required; you have %s'
865 % ('.'.join(needed), '.'.join(_ver)))
869 def _git_wait(cmd, p):
872 raise GitError('%s returned %d' % (cmd, rv))
875 def _git_capture(argv):
876 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
878 _git_wait(repr(argv), p)
882 class _AbortableIter:
883 def __init__(self, it, onabort = None):
885 self.onabort = onabort
893 return self.it.next()
894 except StopIteration, e:
902 """Abort iteration and call the abortion callback, if needed."""
914 """Link to 'git cat-file' that is used to retrieve blob data."""
917 wanted = ('1','5','6')
920 log('warning: git version < %s; bup will be slow.\n'
923 self.get = self._slow_get
925 self.p = self.inprogress = None
926 self.get = self._fast_get
930 self.p.stdout.close()
933 self.inprogress = None
937 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
938 stdin=subprocess.PIPE,
939 stdout=subprocess.PIPE,
942 preexec_fn = _gitenv)
944 def _fast_get(self, id):
945 if not self.p or self.p.poll() != None:
948 assert(self.p.poll() == None)
950 log('_fast_get: opening %r while %r is open'
951 % (id, self.inprogress))
952 assert(not self.inprogress)
953 assert(id.find('\n') < 0)
954 assert(id.find('\r') < 0)
955 assert(not id.startswith('-'))
957 self.p.stdin.write('%s\n' % id)
959 hdr = self.p.stdout.readline()
960 if hdr.endswith(' missing\n'):
961 self.inprogress = None
962 raise KeyError('blob %r is missing' % id)
964 if len(spl) != 3 or len(spl[0]) != 40:
965 raise GitError('expected blob, got %r' % spl)
966 (hex, type, size) = spl
968 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
969 onabort = self._abort)
974 assert(self.p.stdout.readline() == '\n')
975 self.inprogress = None
980 def _slow_get(self, id):
981 assert(id.find('\n') < 0)
982 assert(id.find('\r') < 0)
984 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
987 p = subprocess.Popen(['git', 'cat-file', type, id],
988 stdout=subprocess.PIPE,
989 preexec_fn = _gitenv)
990 for blob in chunkyreader(p.stdout):
992 _git_wait('git cat-file', p)
1000 treefile = ''.join(it)
1001 for (mode, name, sha) in tree_decode(treefile):
1002 for blob in self.join(sha.encode('hex')):
1004 elif type == 'commit':
1005 treeline = ''.join(it).split('\n')[0]
1006 assert(treeline.startswith('tree '))
1007 for blob in self.join(treeline[5:]):
1010 raise GitError('invalid object type %r: expected blob/tree/commit'
1014 """Generate a list of the content of all blobs that can be reached
1015 from an object. The hash given in 'id' must point to a blob, a tree
1016 or a commit. The content of all blobs that can be seen from trees or
1017 commits will be added to the list.
1020 for d in self._join(self.get(id)):
1022 except StopIteration:
1026 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1028 for (n,c) in list_refs():
1029 if n.startswith('refs/tags/'):
1034 tags[c].append(name) # more than one tag can point at 'c'