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_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), -xstat.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.
835 os.stat(repo('objects/pack/.'))
837 if e.errno == errno.ENOENT:
838 if repodir != home_repodir:
839 log('error: %r is not a bup/git repository\n' % repo())
844 log('error: %s\n' % e)
850 """Get Git's version and ensure a usable version is installed.
852 The returned version is formatted as an ordered tuple with each position
853 representing a digit in the version tag. For example, the following tuple
854 would represent version 1.6.6.9:
860 p = subprocess.Popen(['git', '--version'],
861 stdout=subprocess.PIPE)
862 gvs = p.stdout.read()
863 _git_wait('git --version', p)
864 m = re.match(r'git version (\S+.\S+)', gvs)
866 raise GitError('git --version weird output: %r' % gvs)
867 _ver = tuple(m.group(1).split('.'))
868 needed = ('1','5', '3', '1')
870 raise GitError('git version %s or higher is required; you have %s'
871 % ('.'.join(needed), '.'.join(_ver)))
875 def _git_wait(cmd, p):
878 raise GitError('%s returned %d' % (cmd, rv))
881 def _git_capture(argv):
882 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
884 _git_wait(repr(argv), p)
888 class _AbortableIter:
889 def __init__(self, it, onabort = None):
891 self.onabort = onabort
899 return self.it.next()
900 except StopIteration, e:
908 """Abort iteration and call the abortion callback, if needed."""
920 """Link to 'git cat-file' that is used to retrieve blob data."""
923 wanted = ('1','5','6')
926 log('warning: git version < %s; bup will be slow.\n'
929 self.get = self._slow_get
931 self.p = self.inprogress = None
932 self.get = self._fast_get
936 self.p.stdout.close()
939 self.inprogress = None
943 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
944 stdin=subprocess.PIPE,
945 stdout=subprocess.PIPE,
948 preexec_fn = _gitenv)
950 def _fast_get(self, id):
951 if not self.p or self.p.poll() != None:
954 assert(self.p.poll() == None)
956 log('_fast_get: opening %r while %r is open'
957 % (id, self.inprogress))
958 assert(not self.inprogress)
959 assert(id.find('\n') < 0)
960 assert(id.find('\r') < 0)
961 assert(not id.startswith('-'))
963 self.p.stdin.write('%s\n' % id)
965 hdr = self.p.stdout.readline()
966 if hdr.endswith(' missing\n'):
967 self.inprogress = None
968 raise KeyError('blob %r is missing' % id)
970 if len(spl) != 3 or len(spl[0]) != 40:
971 raise GitError('expected blob, got %r' % spl)
972 (hex, type, size) = spl
974 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
975 onabort = self._abort)
980 assert(self.p.stdout.readline() == '\n')
981 self.inprogress = None
986 def _slow_get(self, id):
987 assert(id.find('\n') < 0)
988 assert(id.find('\r') < 0)
990 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
993 p = subprocess.Popen(['git', 'cat-file', type, id],
994 stdout=subprocess.PIPE,
995 preexec_fn = _gitenv)
996 for blob in chunkyreader(p.stdout):
998 _git_wait('git cat-file', p)
1000 def _join(self, it):
1005 elif type == 'tree':
1006 treefile = ''.join(it)
1007 for (mode, name, sha) in tree_decode(treefile):
1008 for blob in self.join(sha.encode('hex')):
1010 elif type == 'commit':
1011 treeline = ''.join(it).split('\n')[0]
1012 assert(treeline.startswith('tree '))
1013 for blob in self.join(treeline[5:]):
1016 raise GitError('invalid object type %r: expected blob/tree/commit'
1020 """Generate a list of the content of all blobs that can be reached
1021 from an object. The hash given in 'id' must point to a blob, a tree
1022 or a commit. The content of all blobs that can be seen from trees or
1023 commits will be added to the list.
1026 for d in self._join(self.get(id)):
1028 except StopIteration:
1032 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1034 for (n,c) in list_refs():
1035 if n.startswith('refs/tags/'):
1040 tags[c].append(name) # more than one tag can point at 'c'