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):
657 idx_f = open(filename, 'w+b')
658 idx_f.write('\377tOc\0\0\0\2')
660 ofs64_ofs = 8 + 4*256 + 28*self.count
661 idx_f.truncate(ofs64_ofs)
663 idx_map = mmap_readwrite(idx_f, close=False)
664 idx_f.seek(0, os.SEEK_END)
665 count = _helpers.write_idx(idx_f, idx_map, idx, self.count)
666 assert(count == self.count)
667 # Sync, since it doesn't look like POSIX guarantees that a
668 # matching FILE* (i.e. idx_f) will see the parallel changes if
676 b = idx_f.read(8 + 4*256)
679 obj_list_sum = Sha1()
680 for b in chunkyreader(idx_f, 20*self.count):
682 obj_list_sum.update(b)
683 namebase = obj_list_sum.hexdigest()
685 for b in chunkyreader(idx_f):
687 idx_f.write(idx_sum.digest())
694 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
698 os.environ['GIT_DIR'] = os.path.abspath(repo())
701 def list_refs(refname = None):
702 """Generate a list of tuples in the form (refname,hash).
703 If a ref name is specified, list only this particular ref.
705 argv = ['git', 'show-ref', '--']
708 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
709 out = p.stdout.read().strip()
710 rv = p.wait() # not fatal
714 for d in out.split('\n'):
715 (sha, name) = d.split(' ', 1)
716 yield (name, sha.decode('hex'))
719 def read_ref(refname):
720 """Get the commit id of the most recent commit made on a given ref."""
721 l = list(list_refs(refname))
729 def rev_list(ref, count=None):
730 """Generate a list of reachable commits in reverse chronological order.
732 This generator walks through commits, from child to parent, that are
733 reachable via the specified ref and yields a series of tuples of the form
736 If count is a non-zero integer, limit the number of commits to "count"
739 assert(not ref.startswith('-'))
742 opts += ['-n', str(atoi(count))]
743 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
744 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
748 if s.startswith('commit '):
749 commit = s[7:].decode('hex')
753 rv = p.wait() # not fatal
755 raise GitError, 'git rev-list returned error %d' % rv
758 def rev_get_date(ref):
759 """Get the date of the latest commit on the specified ref."""
760 for (date, commit) in rev_list(ref, count=1):
762 raise GitError, 'no such commit %r' % ref
765 def rev_parse(committish):
766 """Resolve the full hash for 'committish', if it exists.
768 Should be roughly equivalent to 'git rev-parse'.
770 Returns the hex value of the hash if it is found, None if 'committish' does
771 not correspond to anything.
773 head = read_ref(committish)
775 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
778 pL = PackIdxList(repo('objects/pack'))
780 if len(committish) == 40:
782 hash = committish.decode('hex')
792 def update_ref(refname, newval, oldval):
793 """Change the commit pointed to by a branch."""
796 assert(refname.startswith('refs/heads/'))
797 p = subprocess.Popen(['git', 'update-ref', refname,
798 newval.encode('hex'), oldval.encode('hex')],
799 preexec_fn = _gitenv)
800 _git_wait('git update-ref', p)
803 def guess_repo(path=None):
804 """Set the path value in the global variable "repodir".
805 This makes bup look for an existing bup repository, but not fail if a
806 repository doesn't exist. Usually, if you are interacting with a bup
807 repository, you would not be calling this function but using
814 repodir = os.environ.get('BUP_DIR')
816 repodir = os.path.expanduser('~/.bup')
819 def init_repo(path=None):
820 """Create the Git bare repository for bup in a given path."""
822 d = repo() # appends a / to the path
823 parent = os.path.dirname(os.path.dirname(d))
824 if parent and not os.path.exists(parent):
825 raise GitError('parent directory "%s" does not exist\n' % parent)
826 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
827 raise GitError('"%s" exists but is not a directory\n' % d)
828 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
829 preexec_fn = _gitenv)
830 _git_wait('git init', p)
831 # Force the index version configuration in order to ensure bup works
832 # regardless of the version of the installed Git binary.
833 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
834 stdout=sys.stderr, preexec_fn = _gitenv)
835 _git_wait('git config', p)
837 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
838 stdout=sys.stderr, preexec_fn = _gitenv)
839 _git_wait('git config', p)
842 def check_repo_or_die(path=None):
843 """Make sure a bup repository exists, and abort if not.
844 If the path to a particular repository was not specified, this function
845 initializes the default repository automatically.
849 os.stat(repo('objects/pack/.'))
851 if e.errno == errno.ENOENT:
852 if repodir != home_repodir:
853 log('error: %r is not a bup repository; run "bup init"\n'
859 log('error: %s\n' % e)
865 """Get Git's version and ensure a usable version is installed.
867 The returned version is formatted as an ordered tuple with each position
868 representing a digit in the version tag. For example, the following tuple
869 would represent version 1.6.6.9:
875 p = subprocess.Popen(['git', '--version'],
876 stdout=subprocess.PIPE)
877 gvs = p.stdout.read()
878 _git_wait('git --version', p)
879 m = re.match(r'git version (\S+.\S+)', gvs)
881 raise GitError('git --version weird output: %r' % gvs)
882 _ver = tuple(m.group(1).split('.'))
883 needed = ('1','5', '3', '1')
885 raise GitError('git version %s or higher is required; you have %s'
886 % ('.'.join(needed), '.'.join(_ver)))
890 def _git_wait(cmd, p):
893 raise GitError('%s returned %d' % (cmd, rv))
896 def _git_capture(argv):
897 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
899 _git_wait(repr(argv), p)
903 class _AbortableIter:
904 def __init__(self, it, onabort = None):
906 self.onabort = onabort
914 return self.it.next()
915 except StopIteration, e:
923 """Abort iteration and call the abortion callback, if needed."""
935 """Link to 'git cat-file' that is used to retrieve blob data."""
938 wanted = ('1','5','6')
941 log('warning: git version < %s; bup will be slow.\n'
944 self.get = self._slow_get
946 self.p = self.inprogress = None
947 self.get = self._fast_get
951 self.p.stdout.close()
954 self.inprogress = None
958 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
959 stdin=subprocess.PIPE,
960 stdout=subprocess.PIPE,
963 preexec_fn = _gitenv)
965 def _fast_get(self, id):
966 if not self.p or self.p.poll() != None:
969 poll_result = self.p.poll()
970 assert(poll_result == None)
972 log('_fast_get: opening %r while %r is open\n'
973 % (id, self.inprogress))
974 assert(not self.inprogress)
975 assert(id.find('\n') < 0)
976 assert(id.find('\r') < 0)
977 assert(not id.startswith('-'))
979 self.p.stdin.write('%s\n' % id)
981 hdr = self.p.stdout.readline()
982 if hdr.endswith(' missing\n'):
983 self.inprogress = None
984 raise KeyError('blob %r is missing' % id)
986 if len(spl) != 3 or len(spl[0]) != 40:
987 raise GitError('expected blob, got %r' % spl)
988 (hex, type, size) = spl
990 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
991 onabort = self._abort)
996 readline_result = self.p.stdout.readline()
997 assert(readline_result == '\n')
998 self.inprogress = None
1003 def _slow_get(self, id):
1004 assert(id.find('\n') < 0)
1005 assert(id.find('\r') < 0)
1006 assert(id[0] != '-')
1007 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1010 p = subprocess.Popen(['git', 'cat-file', type, id],
1011 stdout=subprocess.PIPE,
1012 preexec_fn = _gitenv)
1013 for blob in chunkyreader(p.stdout):
1015 _git_wait('git cat-file', p)
1017 def _join(self, it):
1022 elif type == 'tree':
1023 treefile = ''.join(it)
1024 for (mode, name, sha) in tree_decode(treefile):
1025 for blob in self.join(sha.encode('hex')):
1027 elif type == 'commit':
1028 treeline = ''.join(it).split('\n')[0]
1029 assert(treeline.startswith('tree '))
1030 for blob in self.join(treeline[5:]):
1033 raise GitError('invalid object type %r: expected blob/tree/commit'
1037 """Generate a list of the content of all blobs that can be reached
1038 from an object. The hash given in 'id' must point to a blob, a tree
1039 or a commit. The content of all blobs that can be seen from trees or
1040 commits will be added to the list.
1043 for d in self._join(self.get(id)):
1045 except StopIteration:
1049 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1051 for (n,c) in list_refs():
1052 if n.startswith('refs/tags/'):
1057 tags[c].append(name) # more than one tag can point at 'c'