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, compression_level=1):
171 szbits = (sz & 0x0f) | (_typemap[type]<<4)
174 if sz: szbits |= 0x80
180 z = zlib.compressobj(compression_level)
182 yield z.compress(content)
186 def _encode_looseobj(type, content, compression_level=1):
187 z = zlib.compressobj(compression_level)
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, compression_level=1):
498 self.objcache_maker = objcache_maker
500 self.compression_level = compression_level
507 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
508 self.file = os.fdopen(fd, 'w+b')
509 assert(name.endswith('.pack'))
510 self.filename = name[:-5]
511 self.file.write('PACK\0\0\0\2\0\0\0\0')
512 self.idx = list(list() for i in xrange(256))
514 def _raw_write(self, datalist, sha):
517 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
518 # the file never has a *partial* blob. So let's make sure it's
519 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
520 # to our hashsplit algorithm.) f.write() does its own buffering,
521 # but that's okay because we'll flush it in _end().
522 oneblob = ''.join(datalist)
526 raise GitError, e, sys.exc_info()[2]
528 crc = zlib.crc32(oneblob) & 0xffffffff
529 self._update_idx(sha, crc, nw)
534 def _update_idx(self, sha, crc, size):
537 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
539 def _write(self, sha, type, content):
543 sha = calc_hash(type, content)
544 size, crc = self._raw_write(_encode_packobj(type, content,
545 self.compression_level),
547 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
551 def breakpoint(self):
552 """Clear byte and object counts and return the last processed id."""
554 self.outbytes = self.count = 0
557 def _require_objcache(self):
558 if self.objcache is None and self.objcache_maker:
559 self.objcache = self.objcache_maker()
560 if self.objcache is None:
562 "PackWriter not opened or can't check exists w/o objcache")
564 def exists(self, id, want_source=False):
565 """Return non-empty if an object is found in the object cache."""
566 self._require_objcache()
567 return self.objcache.exists(id, want_source=want_source)
569 def maybe_write(self, type, content):
570 """Write an object to the pack file if not present and return its id."""
571 sha = calc_hash(type, content)
572 if not self.exists(sha):
573 self._write(sha, type, content)
574 self._require_objcache()
575 self.objcache.add(sha)
578 def new_blob(self, blob):
579 """Create a blob object in the pack with the supplied content."""
580 return self.maybe_write('blob', blob)
582 def new_tree(self, shalist):
583 """Create a tree object in the pack."""
584 content = tree_encode(shalist)
585 return self.maybe_write('tree', content)
587 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
589 if tree: l.append('tree %s' % tree.encode('hex'))
590 if parent: l.append('parent %s' % parent.encode('hex'))
591 if author: l.append('author %s %s' % (author, _git_date(adate)))
592 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
595 return self.maybe_write('commit', '\n'.join(l))
597 def new_commit(self, parent, tree, date, msg):
598 """Create a commit object in the pack."""
599 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
600 commit = self._new_commit(tree, parent,
601 userline, date, userline, date,
606 """Remove the pack file from disk."""
612 os.unlink(self.filename + '.pack')
614 def _end(self, run_midx=True):
616 if not f: return None
622 # update object count
624 cp = struct.pack('!i', self.count)
628 # calculate the pack sha1sum
631 for b in chunkyreader(f):
633 packbin = sum.digest()
637 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
639 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
640 if os.path.exists(self.filename + '.map'):
641 os.unlink(self.filename + '.map')
642 os.rename(self.filename + '.pack', nameprefix + '.pack')
643 os.rename(self.filename + '.idx', nameprefix + '.idx')
646 auto_midx(repo('objects/pack'))
649 def close(self, run_midx=True):
650 """Close the pack file and move it to its definitive path."""
651 return self._end(run_midx=run_midx)
653 def _write_pack_idx_v2(self, filename, idx, packbin):
654 idx_f = open(filename, 'w+b')
655 idx_f.write('\377tOc\0\0\0\2')
657 ofs64_ofs = 8 + 4*256 + 28*self.count
658 idx_f.truncate(ofs64_ofs)
660 idx_map = mmap_readwrite(idx_f, close=False)
661 idx_f.seek(0, SEEK_END)
662 count = _helpers.write_idx(idx_f, idx_map, idx, self.count)
663 assert(count == self.count)
669 b = idx_f.read(8 + 4*256)
672 obj_list_sum = Sha1()
673 for b in chunkyreader(idx_f, 20*self.count):
675 obj_list_sum.update(b)
676 namebase = obj_list_sum.hexdigest()
678 for b in chunkyreader(idx_f):
680 idx_f.write(idx_sum.digest())
687 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
691 os.environ['GIT_DIR'] = os.path.abspath(repo())
694 def list_refs(refname = None):
695 """Generate a list of tuples in the form (refname,hash).
696 If a ref name is specified, list only this particular ref.
698 argv = ['git', 'show-ref', '--']
701 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
702 out = p.stdout.read().strip()
703 rv = p.wait() # not fatal
707 for d in out.split('\n'):
708 (sha, name) = d.split(' ', 1)
709 yield (name, sha.decode('hex'))
712 def read_ref(refname):
713 """Get the commit id of the most recent commit made on a given ref."""
714 l = list(list_refs(refname))
722 def rev_list(ref, count=None):
723 """Generate a list of reachable commits in reverse chronological order.
725 This generator walks through commits, from child to parent, that are
726 reachable via the specified ref and yields a series of tuples of the form
729 If count is a non-zero integer, limit the number of commits to "count"
732 assert(not ref.startswith('-'))
735 opts += ['-n', str(atoi(count))]
736 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
737 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
741 if s.startswith('commit '):
742 commit = s[7:].decode('hex')
746 rv = p.wait() # not fatal
748 raise GitError, 'git rev-list returned error %d' % rv
751 def rev_get_date(ref):
752 """Get the date of the latest commit on the specified ref."""
753 for (date, commit) in rev_list(ref, count=1):
755 raise GitError, 'no such commit %r' % ref
758 def rev_parse(committish):
759 """Resolve the full hash for 'committish', if it exists.
761 Should be roughly equivalent to 'git rev-parse'.
763 Returns the hex value of the hash if it is found, None if 'committish' does
764 not correspond to anything.
766 head = read_ref(committish)
768 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
771 pL = PackIdxList(repo('objects/pack'))
773 if len(committish) == 40:
775 hash = committish.decode('hex')
785 def update_ref(refname, newval, oldval):
786 """Change the commit pointed to by a branch."""
789 assert(refname.startswith('refs/heads/'))
790 p = subprocess.Popen(['git', 'update-ref', refname,
791 newval.encode('hex'), oldval.encode('hex')],
792 preexec_fn = _gitenv)
793 _git_wait('git update-ref', p)
796 def guess_repo(path=None):
797 """Set the path value in the global variable "repodir".
798 This makes bup look for an existing bup repository, but not fail if a
799 repository doesn't exist. Usually, if you are interacting with a bup
800 repository, you would not be calling this function but using
807 repodir = os.environ.get('BUP_DIR')
809 repodir = os.path.expanduser('~/.bup')
812 def init_repo(path=None):
813 """Create the Git bare repository for bup in a given path."""
815 d = repo() # appends a / to the path
816 parent = os.path.dirname(os.path.dirname(d))
817 if parent and not os.path.exists(parent):
818 raise GitError('parent directory "%s" does not exist\n' % parent)
819 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
820 raise GitError('"%d" exists but is not a directory\n' % d)
821 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
822 preexec_fn = _gitenv)
823 _git_wait('git init', p)
824 # Force the index version configuration in order to ensure bup works
825 # regardless of the version of the installed Git binary.
826 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
827 stdout=sys.stderr, preexec_fn = _gitenv)
828 _git_wait('git config', p)
831 def check_repo_or_die(path=None):
832 """Make sure a bup repository exists, and abort if not.
833 If the path to a particular repository was not specified, this function
834 initializes the default repository automatically.
838 os.stat(repo('objects/pack/.'))
840 if e.errno == errno.ENOENT:
841 if repodir != home_repodir:
842 log('error: %r is not a bup/git repository\n' % repo())
847 log('error: %s\n' % e)
853 """Get Git's version and ensure a usable version is installed.
855 The returned version is formatted as an ordered tuple with each position
856 representing a digit in the version tag. For example, the following tuple
857 would represent version 1.6.6.9:
863 p = subprocess.Popen(['git', '--version'],
864 stdout=subprocess.PIPE)
865 gvs = p.stdout.read()
866 _git_wait('git --version', p)
867 m = re.match(r'git version (\S+.\S+)', gvs)
869 raise GitError('git --version weird output: %r' % gvs)
870 _ver = tuple(m.group(1).split('.'))
871 needed = ('1','5', '3', '1')
873 raise GitError('git version %s or higher is required; you have %s'
874 % ('.'.join(needed), '.'.join(_ver)))
878 def _git_wait(cmd, p):
881 raise GitError('%s returned %d' % (cmd, rv))
884 def _git_capture(argv):
885 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
887 _git_wait(repr(argv), p)
891 class _AbortableIter:
892 def __init__(self, it, onabort = None):
894 self.onabort = onabort
902 return self.it.next()
903 except StopIteration, e:
911 """Abort iteration and call the abortion callback, if needed."""
923 """Link to 'git cat-file' that is used to retrieve blob data."""
926 wanted = ('1','5','6')
929 log('warning: git version < %s; bup will be slow.\n'
932 self.get = self._slow_get
934 self.p = self.inprogress = None
935 self.get = self._fast_get
939 self.p.stdout.close()
942 self.inprogress = None
946 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
947 stdin=subprocess.PIPE,
948 stdout=subprocess.PIPE,
951 preexec_fn = _gitenv)
953 def _fast_get(self, id):
954 if not self.p or self.p.poll() != None:
957 assert(self.p.poll() == None)
959 log('_fast_get: opening %r while %r is open'
960 % (id, self.inprogress))
961 assert(not self.inprogress)
962 assert(id.find('\n') < 0)
963 assert(id.find('\r') < 0)
964 assert(not id.startswith('-'))
966 self.p.stdin.write('%s\n' % id)
968 hdr = self.p.stdout.readline()
969 if hdr.endswith(' missing\n'):
970 self.inprogress = None
971 raise KeyError('blob %r is missing' % id)
973 if len(spl) != 3 or len(spl[0]) != 40:
974 raise GitError('expected blob, got %r' % spl)
975 (hex, type, size) = spl
977 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
978 onabort = self._abort)
983 assert(self.p.stdout.readline() == '\n')
984 self.inprogress = None
989 def _slow_get(self, id):
990 assert(id.find('\n') < 0)
991 assert(id.find('\r') < 0)
993 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
996 p = subprocess.Popen(['git', 'cat-file', type, id],
997 stdout=subprocess.PIPE,
998 preexec_fn = _gitenv)
999 for blob in chunkyreader(p.stdout):
1001 _git_wait('git cat-file', p)
1003 def _join(self, it):
1008 elif type == 'tree':
1009 treefile = ''.join(it)
1010 for (mode, name, sha) in tree_decode(treefile):
1011 for blob in self.join(sha.encode('hex')):
1013 elif type == 'commit':
1014 treeline = ''.join(it).split('\n')[0]
1015 assert(treeline.startswith('tree '))
1016 for blob in self.join(treeline[5:]):
1019 raise GitError('invalid object type %r: expected blob/tree/commit'
1023 """Generate a list of the content of all blobs that can be reached
1024 from an object. The hash given in 'id' must point to a blob, a tree
1025 or a commit. The content of all blobs that can be seen from trees or
1026 commits will be added to the list.
1029 for d in self._join(self.get(id)):
1031 except StopIteration:
1035 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1037 for (n,c) in list_refs():
1038 if n.startswith('refs/tags/'):
1043 tags[c].append(name) # more than one tag can point at 'c'