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
16 _typemap = { 'blob':3, 'tree':2, 'commit':1, 'tag':4 }
17 _typermap = { 3:'blob', 2:'tree', 1:'commit', 4:'tag' }
23 class GitError(Exception):
28 """Get the path to the git repository or one of its subdirectories."""
31 raise GitError('You should call check_repo_or_die()')
33 # If there's a .git subdirectory, then the actual repo is in there.
34 gd = os.path.join(repodir, '.git')
35 if os.path.exists(gd):
38 return os.path.join(repodir, sub)
42 return re.sub(r'([^0-9a-z]|\b)([0-9a-z]{7})[0-9a-z]{33}([^0-9a-z]|\b)',
47 full = os.path.abspath(path)
48 fullrepo = os.path.abspath(repo(''))
49 if not fullrepo.endswith('/'):
51 if full.startswith(fullrepo):
52 path = full[len(fullrepo):]
53 if path.startswith('index-cache/'):
54 path = path[len('index-cache/'):]
55 return shorten_hash(path)
59 paths = [repo('objects/pack')]
60 paths += glob.glob(repo('index-cache/*/.'))
64 def auto_midx(objdir):
65 args = [path.exe(), 'midx', '--auto', '--dir', objdir]
67 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
69 # make sure 'args' gets printed to help with debugging
70 add_error('%r: exception: %s' % (args, e))
73 add_error('%r: returned %d' % (args, rv))
75 args = [path.exe(), 'bloom', '--dir', objdir]
77 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
79 # make sure 'args' gets printed to help with debugging
80 add_error('%r: exception: %s' % (args, e))
83 add_error('%r: returned %d' % (args, rv))
86 def mangle_name(name, mode, gitmode):
87 """Mangle a file name to present an abstract name for segmented files.
88 Mangled file names will have the ".bup" extension added to them. If a
89 file's name already ends with ".bup", a ".bupl" extension is added to
90 disambiguate normal files from semgmented ones.
92 if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
94 elif name.endswith('.bup') or name[:-1].endswith('.bup'):
100 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
101 def demangle_name(name):
102 """Remove name mangling from a file name, if necessary.
104 The return value is a tuple (demangled_filename,mode), where mode is one of
107 * BUP_NORMAL : files that should be read as-is from the repository
108 * BUP_CHUNKED : files that were chunked and need to be assembled
110 For more information on the name mangling algorythm, see mangle_name()
112 if name.endswith('.bupl'):
113 return (name[:-5], BUP_NORMAL)
114 elif name.endswith('.bup'):
115 return (name[:-4], BUP_CHUNKED)
117 return (name, BUP_NORMAL)
120 def calc_hash(type, content):
121 """Calculate some content's hash in the Git fashion."""
122 header = '%s %d\0' % (type, len(content))
128 def shalist_item_sort_key(ent):
129 (mode, name, id) = ent
130 assert(mode+0 == mode)
131 if stat.S_ISDIR(mode):
137 def tree_encode(shalist):
138 """Generate a git tree object from (mode,name,hash) tuples."""
139 shalist = sorted(shalist, key = shalist_item_sort_key)
141 for (mode,name,bin) in shalist:
143 assert(mode+0 == mode)
145 assert(len(bin) == 20)
146 s = '%o %s\0%s' % (mode,name,bin)
147 assert(s[0] != '0') # 0-padded octal is not acceptable in a git tree
152 def tree_decode(buf):
153 """Generate a list of (mode,name,hash) from the git tree object in buf."""
155 while ofs < len(buf):
156 z = buf.find('\0', ofs)
158 spl = buf[ofs:z].split(' ', 1)
159 assert(len(spl) == 2)
161 sha = buf[z+1:z+1+20]
163 yield (int(mode, 8), name, sha)
166 def _encode_packobj(type, content, compression_level=1):
169 szbits = (sz & 0x0f) | (_typemap[type]<<4)
172 if sz: szbits |= 0x80
178 if compression_level > 9:
179 compression_level = 9
180 elif compression_level < 0:
181 compression_level = 0
182 z = zlib.compressobj(compression_level)
184 yield z.compress(content)
188 def _encode_looseobj(type, content, compression_level=1):
189 z = zlib.compressobj(compression_level)
190 yield z.compress('%s %d\0' % (type, len(content)))
191 yield z.compress(content)
195 def _decode_looseobj(buf):
197 s = zlib.decompress(buf)
204 assert(type in _typemap)
205 assert(sz == len(content))
206 return (type, content)
209 def _decode_packobj(buf):
212 type = _typermap[(c & 0x70) >> 4]
219 sz |= (c & 0x7f) << shift
223 return (type, zlib.decompress(buf[i+1:]))
230 def find_offset(self, hash):
231 """Get the offset of an object inside the index file."""
232 idx = self._idx_from_hash(hash)
234 return self._ofs_from_idx(idx)
237 def exists(self, hash, want_source=False):
238 """Return nonempty if the object exists in this index."""
239 if hash and (self._idx_from_hash(hash) != None):
240 return want_source and os.path.basename(self.name) or True
244 return int(self.fanout[255])
246 def _idx_from_hash(self, hash):
247 global _total_searches, _total_steps
249 assert(len(hash) == 20)
251 start = self.fanout[b1-1] # range -1..254
252 end = self.fanout[b1] # range 0..255
254 _total_steps += 1 # lookup table is a step
257 mid = start + (end-start)/2
258 v = self._idx_to_hash(mid)
268 class PackIdxV1(PackIdx):
269 """Object representation of a Git pack index (version 1) file."""
270 def __init__(self, filename, f):
272 self.idxnames = [self.name]
273 self.map = mmap_read(f)
274 self.fanout = list(struct.unpack('!256I',
275 str(buffer(self.map, 0, 256*4))))
276 self.fanout.append(0) # entry "-1"
277 nsha = self.fanout[255]
279 self.shatable = buffer(self.map, self.sha_ofs, nsha*24)
281 def _ofs_from_idx(self, idx):
282 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
284 def _idx_to_hash(self, idx):
285 return str(self.shatable[idx*24+4 : idx*24+24])
288 for i in xrange(self.fanout[255]):
289 yield buffer(self.map, 256*4 + 24*i + 4, 20)
292 class PackIdxV2(PackIdx):
293 """Object representation of a Git pack index (version 2) file."""
294 def __init__(self, filename, f):
296 self.idxnames = [self.name]
297 self.map = mmap_read(f)
298 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
299 self.fanout = list(struct.unpack('!256I',
300 str(buffer(self.map, 8, 256*4))))
301 self.fanout.append(0) # entry "-1"
302 nsha = self.fanout[255]
303 self.sha_ofs = 8 + 256*4
304 self.shatable = buffer(self.map, self.sha_ofs, nsha*20)
305 self.ofstable = buffer(self.map,
306 self.sha_ofs + nsha*20 + nsha*4,
308 self.ofs64table = buffer(self.map,
309 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
311 def _ofs_from_idx(self, idx):
312 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
314 idx64 = ofs & 0x7fffffff
315 ofs = struct.unpack('!Q',
316 str(buffer(self.ofs64table, idx64*8, 8)))[0]
319 def _idx_to_hash(self, idx):
320 return str(self.shatable[idx*20:(idx+1)*20])
323 for i in xrange(self.fanout[255]):
324 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
329 def __init__(self, dir):
331 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
336 self.do_bloom = False
343 assert(_mpi_count == 0)
346 return iter(idxmerge(self.packs))
349 return sum(len(pack) for pack in self.packs)
351 def exists(self, hash, want_source=False):
352 """Return nonempty if the object exists in the index files."""
353 global _total_searches
355 if hash in self.also:
357 if self.do_bloom and self.bloom:
358 if self.bloom.exists(hash):
359 self.do_bloom = False
361 _total_searches -= 1 # was counted by bloom
363 for i in xrange(len(self.packs)):
365 _total_searches -= 1 # will be incremented by sub-pack
366 ix = p.exists(hash, want_source=want_source)
368 # reorder so most recently used packs are searched first
369 self.packs = [p] + self.packs[:i] + self.packs[i+1:]
374 def refresh(self, skip_midx = False):
375 """Refresh the index list.
376 This method verifies if .midx files were superseded (e.g. all of its
377 contents are in another, bigger .midx file) and removes the superseded
380 If skip_midx is True, all work on .midx files will be skipped and .midx
381 files will be removed from the list.
383 The module-global variable 'ignore_midx' can force this function to
384 always act as if skip_midx was True.
386 self.bloom = None # Always reopen the bloom as it may have been relaced
387 self.do_bloom = False
388 skip_midx = skip_midx or ignore_midx
389 d = dict((p.name, p) for p in self.packs
390 if not skip_midx or not isinstance(p, midx.PackMidx))
391 if os.path.exists(self.dir):
394 for ix in self.packs:
395 if isinstance(ix, midx.PackMidx):
396 for name in ix.idxnames:
397 d[os.path.join(self.dir, name)] = ix
398 for full in glob.glob(os.path.join(self.dir,'*.midx')):
400 mx = midx.PackMidx(full)
401 (mxd, mxf) = os.path.split(mx.name)
403 for n in mx.idxnames:
404 if not os.path.exists(os.path.join(mxd, n)):
405 log(('warning: index %s missing\n' +
406 ' used by %s\n') % (n, mxf))
413 midxl.sort(key=lambda ix:
414 (-len(ix), -xstat.stat(ix.name).st_mtime))
417 for sub in ix.idxnames:
418 found = d.get(os.path.join(self.dir, sub))
419 if not found or isinstance(found, PackIdx):
420 # doesn't exist, or exists but not in a midx
425 for name in ix.idxnames:
426 d[os.path.join(self.dir, name)] = ix
427 elif not ix.force_keep:
428 debug1('midx: removing redundant: %s\n'
429 % os.path.basename(ix.name))
431 for full in glob.glob(os.path.join(self.dir,'*.idx')):
439 bfull = os.path.join(self.dir, 'bup.bloom')
440 if self.bloom is None and os.path.exists(bfull):
441 self.bloom = bloom.ShaBloom(bfull)
442 self.packs = list(set(d.values()))
443 self.packs.sort(lambda x,y: -cmp(len(x),len(y)))
444 if self.bloom and self.bloom.valid() and len(self.bloom) >= len(self):
448 debug1('PackIdxList: using %d index%s.\n'
449 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
452 """Insert an additional object in the list."""
456 def open_idx(filename):
457 if filename.endswith('.idx'):
458 f = open(filename, 'rb')
460 if header[0:4] == '\377tOc':
461 version = struct.unpack('!I', header[4:8])[0]
463 return PackIdxV2(filename, f)
465 raise GitError('%s: expected idx file version 2, got %d'
466 % (filename, version))
467 elif len(header) == 8 and header[0:4] < '\377tOc':
468 return PackIdxV1(filename, f)
470 raise GitError('%s: unrecognized idx file header' % filename)
471 elif filename.endswith('.midx'):
472 return midx.PackMidx(filename)
474 raise GitError('idx filenames must end with .idx or .midx')
477 def idxmerge(idxlist, final_progress=True):
478 """Generate a list of all the objects reachable in a PackIdxList."""
479 def pfunc(count, total):
480 qprogress('Reading indexes: %.2f%% (%d/%d)\r'
481 % (count*100.0/total, count, total))
482 def pfinal(count, total):
484 progress('Reading indexes: %.2f%% (%d/%d), done.\n'
485 % (100, total, total))
486 return merge_iter(idxlist, 10024, pfunc, pfinal)
489 def _make_objcache():
490 return PackIdxList(repo('objects/pack'))
493 """Writes Git objects inside a pack file."""
494 def __init__(self, objcache_maker=_make_objcache, compression_level=1):
500 self.objcache_maker = objcache_maker
502 self.compression_level = compression_level
509 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
510 self.file = os.fdopen(fd, 'w+b')
511 assert(name.endswith('.pack'))
512 self.filename = name[:-5]
513 self.file.write('PACK\0\0\0\2\0\0\0\0')
514 self.idx = list(list() for i in xrange(256))
516 def _raw_write(self, datalist, sha):
519 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
520 # the file never has a *partial* blob. So let's make sure it's
521 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
522 # to our hashsplit algorithm.) f.write() does its own buffering,
523 # but that's okay because we'll flush it in _end().
524 oneblob = ''.join(datalist)
528 raise GitError, e, sys.exc_info()[2]
530 crc = zlib.crc32(oneblob) & 0xffffffff
531 self._update_idx(sha, crc, nw)
536 def _update_idx(self, sha, crc, size):
539 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
541 def _write(self, sha, type, content):
545 sha = calc_hash(type, content)
546 size, crc = self._raw_write(_encode_packobj(type, content,
547 self.compression_level),
549 if self.outbytes >= max_pack_size or self.count >= max_pack_objects:
553 def breakpoint(self):
554 """Clear byte and object counts and return the last processed id."""
556 self.outbytes = self.count = 0
559 def _require_objcache(self):
560 if self.objcache is None and self.objcache_maker:
561 self.objcache = self.objcache_maker()
562 if self.objcache is None:
564 "PackWriter not opened or can't check exists w/o objcache")
566 def exists(self, id, want_source=False):
567 """Return non-empty if an object is found in the object cache."""
568 self._require_objcache()
569 return self.objcache.exists(id, want_source=want_source)
571 def maybe_write(self, type, content):
572 """Write an object to the pack file if not present and return its id."""
573 sha = calc_hash(type, content)
574 if not self.exists(sha):
575 self._write(sha, type, content)
576 self._require_objcache()
577 self.objcache.add(sha)
580 def new_blob(self, blob):
581 """Create a blob object in the pack with the supplied content."""
582 return self.maybe_write('blob', blob)
584 def new_tree(self, shalist):
585 """Create a tree object in the pack."""
586 content = tree_encode(shalist)
587 return self.maybe_write('tree', content)
589 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
591 if tree: l.append('tree %s' % tree.encode('hex'))
592 if parent: l.append('parent %s' % parent.encode('hex'))
593 if author: l.append('author %s %s' % (author, _git_date(adate)))
594 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
597 return self.maybe_write('commit', '\n'.join(l))
599 def new_commit(self, parent, tree, date, msg):
600 """Create a commit object in the pack."""
601 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
602 commit = self._new_commit(tree, parent,
603 userline, date, userline, date,
608 """Remove the pack file from disk."""
614 os.unlink(self.filename + '.pack')
616 def _end(self, run_midx=True):
618 if not f: return None
624 # update object count
626 cp = struct.pack('!i', self.count)
630 # calculate the pack sha1sum
633 for b in chunkyreader(f):
635 packbin = sum.digest()
639 obj_list_sha = self._write_pack_idx_v2(self.filename + '.idx', idx, packbin)
641 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
642 if os.path.exists(self.filename + '.map'):
643 os.unlink(self.filename + '.map')
644 os.rename(self.filename + '.pack', nameprefix + '.pack')
645 os.rename(self.filename + '.idx', nameprefix + '.idx')
648 auto_midx(repo('objects/pack'))
651 def close(self, run_midx=True):
652 """Close the pack file and move it to its definitive path."""
653 return self._end(run_midx=run_midx)
655 def _write_pack_idx_v2(self, filename, idx, packbin):
658 for entry in section:
659 if entry[2] >= 2**31:
662 # Length: header + fan-out + shas-and-crcs + overflow-offsets
663 index_len = 8 + (4 * 256) + (28 * self.count) + (8 * ofs64_count)
665 idx_f = open(filename, 'w+b')
667 idx_f.truncate(index_len)
668 idx_map = mmap_readwrite(idx_f, close=False)
669 count = _helpers.write_idx(filename, idx_map, idx, self.count)
670 assert(count == self.count)
672 if idx_map: idx_map.close()
675 idx_f = open(filename, 'a+b')
680 b = idx_f.read(8 + 4*256)
683 obj_list_sum = Sha1()
684 for b in chunkyreader(idx_f, 20*self.count):
686 obj_list_sum.update(b)
687 namebase = obj_list_sum.hexdigest()
689 for b in chunkyreader(idx_f):
691 idx_f.write(idx_sum.digest())
698 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
702 os.environ['GIT_DIR'] = os.path.abspath(repo())
705 def list_refs(refname = None):
706 """Generate a list of tuples in the form (refname,hash).
707 If a ref name is specified, list only this particular ref.
709 argv = ['git', 'show-ref', '--']
712 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
713 out = p.stdout.read().strip()
714 rv = p.wait() # not fatal
718 for d in out.split('\n'):
719 (sha, name) = d.split(' ', 1)
720 yield (name, sha.decode('hex'))
723 def read_ref(refname):
724 """Get the commit id of the most recent commit made on a given ref."""
725 l = list(list_refs(refname))
733 def rev_list(ref, count=None):
734 """Generate a list of reachable commits in reverse chronological order.
736 This generator walks through commits, from child to parent, that are
737 reachable via the specified ref and yields a series of tuples of the form
740 If count is a non-zero integer, limit the number of commits to "count"
743 assert(not ref.startswith('-'))
746 opts += ['-n', str(atoi(count))]
747 argv = ['git', 'rev-list', '--pretty=format:%at'] + opts + [ref, '--']
748 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
752 if s.startswith('commit '):
753 commit = s[7:].decode('hex')
757 rv = p.wait() # not fatal
759 raise GitError, 'git rev-list returned error %d' % rv
762 def get_commit_dates(refs):
763 """Get the dates for the specified commit refs. For now, every unique
764 string in refs must resolve to a different commit or this
765 function will fail."""
767 cmd = ['git', 'show', '-s', '--pretty=format:%ct']
768 for chunk in batchpipe(cmd, refs, preexec_fn=_gitenv):
769 result += [int(x) for x in chunk.splitlines()]
770 if len(result) == len(refs):
772 # git show suppressed duplicates -- fix it
774 corrected_result = []
777 prev_date = ref_dates.get(ref)
779 corrected_result.append(prev_date)
782 ref_dates[ref] = date
783 corrected_result.append(date)
784 assert(next(dates, None) is None)
785 return corrected_result
788 def rev_parse(committish):
789 """Resolve the full hash for 'committish', if it exists.
791 Should be roughly equivalent to 'git rev-parse'.
793 Returns the hex value of the hash if it is found, None if 'committish' does
794 not correspond to anything.
796 head = read_ref(committish)
798 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
801 pL = PackIdxList(repo('objects/pack'))
803 if len(committish) == 40:
805 hash = committish.decode('hex')
815 def update_ref(refname, newval, oldval):
816 """Change the commit pointed to by a branch."""
819 assert(refname.startswith('refs/heads/'))
820 p = subprocess.Popen(['git', 'update-ref', refname,
821 newval.encode('hex'), oldval.encode('hex')],
822 preexec_fn = _gitenv)
823 _git_wait('git update-ref', p)
826 def guess_repo(path=None):
827 """Set the path value in the global variable "repodir".
828 This makes bup look for an existing bup repository, but not fail if a
829 repository doesn't exist. Usually, if you are interacting with a bup
830 repository, you would not be calling this function but using
837 repodir = os.environ.get('BUP_DIR')
839 repodir = os.path.expanduser('~/.bup')
842 def init_repo(path=None):
843 """Create the Git bare repository for bup in a given path."""
845 d = repo() # appends a / to the path
846 parent = os.path.dirname(os.path.dirname(d))
847 if parent and not os.path.exists(parent):
848 raise GitError('parent directory "%s" does not exist\n' % parent)
849 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
850 raise GitError('"%s" exists but is not a directory\n' % d)
851 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
852 preexec_fn = _gitenv)
853 _git_wait('git init', p)
854 # Force the index version configuration in order to ensure bup works
855 # regardless of the version of the installed Git binary.
856 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
857 stdout=sys.stderr, preexec_fn = _gitenv)
858 _git_wait('git config', p)
860 p = subprocess.Popen(['git', 'config', 'core.logAllRefUpdates', 'true'],
861 stdout=sys.stderr, preexec_fn = _gitenv)
862 _git_wait('git config', p)
865 def check_repo_or_die(path=None):
866 """Make sure a bup repository exists, and abort if not.
867 If the path to a particular repository was not specified, this function
868 initializes the default repository automatically.
872 os.stat(repo('objects/pack/.'))
874 if e.errno == errno.ENOENT:
875 log('error: %r is not a bup repository; run "bup init"\n'
879 log('error: %s\n' % e)
885 """Get Git's version and ensure a usable version is installed.
887 The returned version is formatted as an ordered tuple with each position
888 representing a digit in the version tag. For example, the following tuple
889 would represent version 1.6.6.9:
895 p = subprocess.Popen(['git', '--version'],
896 stdout=subprocess.PIPE)
897 gvs = p.stdout.read()
898 _git_wait('git --version', p)
899 m = re.match(r'git version (\S+.\S+)', gvs)
901 raise GitError('git --version weird output: %r' % gvs)
902 _ver = tuple(m.group(1).split('.'))
903 needed = ('1','5', '3', '1')
905 raise GitError('git version %s or higher is required; you have %s'
906 % ('.'.join(needed), '.'.join(_ver)))
910 def _git_wait(cmd, p):
913 raise GitError('%s returned %d' % (cmd, rv))
916 def _git_capture(argv):
917 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
919 _git_wait(repr(argv), p)
923 class _AbortableIter:
924 def __init__(self, it, onabort = None):
926 self.onabort = onabort
934 return self.it.next()
935 except StopIteration, e:
943 """Abort iteration and call the abortion callback, if needed."""
955 """Link to 'git cat-file' that is used to retrieve blob data."""
958 wanted = ('1','5','6')
961 log('warning: git version < %s; bup will be slow.\n'
964 self.get = self._slow_get
966 self.p = self.inprogress = None
967 self.get = self._fast_get
971 self.p.stdout.close()
974 self.inprogress = None
978 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
979 stdin=subprocess.PIPE,
980 stdout=subprocess.PIPE,
983 preexec_fn = _gitenv)
985 def _fast_get(self, id):
986 if not self.p or self.p.poll() != None:
989 poll_result = self.p.poll()
990 assert(poll_result == None)
992 log('_fast_get: opening %r while %r is open\n'
993 % (id, self.inprogress))
994 assert(not self.inprogress)
995 assert(id.find('\n') < 0)
996 assert(id.find('\r') < 0)
997 assert(not id.startswith('-'))
999 self.p.stdin.write('%s\n' % id)
1000 self.p.stdin.flush()
1001 hdr = self.p.stdout.readline()
1002 if hdr.endswith(' missing\n'):
1003 self.inprogress = None
1004 raise KeyError('blob %r is missing' % id)
1005 spl = hdr.split(' ')
1006 if len(spl) != 3 or len(spl[0]) != 40:
1007 raise GitError('expected blob, got %r' % spl)
1008 (hex, type, size) = spl
1010 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
1011 onabort = self._abort)
1016 readline_result = self.p.stdout.readline()
1017 assert(readline_result == '\n')
1018 self.inprogress = None
1019 except Exception, e:
1023 def _slow_get(self, id):
1024 assert(id.find('\n') < 0)
1025 assert(id.find('\r') < 0)
1026 assert(id[0] != '-')
1027 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1030 p = subprocess.Popen(['git', 'cat-file', type, id],
1031 stdout=subprocess.PIPE,
1032 preexec_fn = _gitenv)
1033 for blob in chunkyreader(p.stdout):
1035 _git_wait('git cat-file', p)
1037 def _join(self, it):
1042 elif type == 'tree':
1043 treefile = ''.join(it)
1044 for (mode, name, sha) in tree_decode(treefile):
1045 for blob in self.join(sha.encode('hex')):
1047 elif type == 'commit':
1048 treeline = ''.join(it).split('\n')[0]
1049 assert(treeline.startswith('tree '))
1050 for blob in self.join(treeline[5:]):
1053 raise GitError('invalid object type %r: expected blob/tree/commit'
1057 """Generate a list of the content of all blobs that can be reached
1058 from an object. The hash given in 'id' must point to a blob, a tree
1059 or a commit. The content of all blobs that can be seen from trees or
1060 commits will be added to the list.
1063 for d in self._join(self.get(id)):
1065 except StopIteration:
1072 """Create a CatPipe object or reuse an already existing one."""
1075 cur_dir = os.path.realpath(repo())
1076 if cur_dir != cp_dir:
1083 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1085 for (n,c) in list_refs():
1086 if n.startswith('refs/tags/'):
1091 tags[c].append(name) # more than one tag can point at 'c'