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, heapq
6 from bup.helpers import *
7 from bup import _helpers, path
13 home_repodir = os.path.expanduser('~/.bup')
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)
41 def auto_midx(objdir):
42 args = [path.exe(), 'midx', '--auto', '--dir', objdir]
44 rv = subprocess.call(args, stdout=open('/dev/null', 'w'))
46 # make sure 'args' gets printed to help with debugging
47 add_error('%r: exception: %s' % (args, e))
50 add_error('%r: returned %d' % (args, rv))
53 def mangle_name(name, mode, gitmode):
54 """Mangle a file name to present an abstract name for segmented files.
55 Mangled file names will have the ".bup" extension added to them. If a
56 file's name already ends with ".bup", a ".bupl" extension is added to
57 disambiguate normal files from semgmented ones.
59 if stat.S_ISREG(mode) and not stat.S_ISREG(gitmode):
61 elif name.endswith('.bup') or name[:-1].endswith('.bup'):
67 (BUP_NORMAL, BUP_CHUNKED) = (0,1)
68 def demangle_name(name):
69 """Remove name mangling from a file name, if necessary.
71 The return value is a tuple (demangled_filename,mode), where mode is one of
74 * BUP_NORMAL : files that should be read as-is from the repository
75 * BUP_CHUNKED : files that were chunked and need to be assembled
77 For more information on the name mangling algorythm, see mangle_name()
79 if name.endswith('.bupl'):
80 return (name[:-5], BUP_NORMAL)
81 elif name.endswith('.bup'):
82 return (name[:-4], BUP_CHUNKED)
84 return (name, BUP_NORMAL)
87 def _encode_packobj(type, content):
90 szbits = (sz & 0x0f) | (_typemap[type]<<4)
99 z = zlib.compressobj(1)
101 yield z.compress(content)
105 def _encode_looseobj(type, content):
106 z = zlib.compressobj(1)
107 yield z.compress('%s %d\0' % (type, len(content)))
108 yield z.compress(content)
112 def _decode_looseobj(buf):
114 s = zlib.decompress(buf)
121 assert(type in _typemap)
122 assert(sz == len(content))
123 return (type, content)
126 def _decode_packobj(buf):
129 type = _typermap[(c & 0x70) >> 4]
136 sz |= (c & 0x7f) << shift
140 return (type, zlib.decompress(buf[i+1:]))
147 def find_offset(self, hash):
148 """Get the offset of an object inside the index file."""
149 idx = self._idx_from_hash(hash)
151 return self._ofs_from_idx(idx)
154 def exists(self, hash):
155 """Return nonempty if the object exists in this index."""
156 return hash and (self._idx_from_hash(hash) != None) and True or None
159 return int(self.fanout[255])
161 def _idx_from_hash(self, hash):
162 global _total_searches, _total_steps
164 assert(len(hash) == 20)
166 start = self.fanout[b1-1] # range -1..254
167 end = self.fanout[b1] # range 0..255
169 _total_steps += 1 # lookup table is a step
172 mid = start + (end-start)/2
173 v = self._idx_to_hash(mid)
183 class PackIdxV1(PackIdx):
184 """Object representation of a Git pack index (version 1) file."""
185 def __init__(self, filename, f):
187 self.idxnames = [self.name]
188 self.map = mmap_read(f)
189 self.fanout = list(struct.unpack('!256I',
190 str(buffer(self.map, 0, 256*4))))
191 self.fanout.append(0) # entry "-1"
192 nsha = self.fanout[255]
193 self.shatable = buffer(self.map, 256*4, nsha*24)
195 def _ofs_from_idx(self, idx):
196 return struct.unpack('!I', str(self.shatable[idx*24 : idx*24+4]))[0]
198 def _idx_to_hash(self, idx):
199 return str(self.shatable[idx*24+4 : idx*24+24])
202 for i in xrange(self.fanout[255]):
203 yield buffer(self.map, 256*4 + 24*i + 4, 20)
206 class PackIdxV2(PackIdx):
207 """Object representation of a Git pack index (version 2) file."""
208 def __init__(self, filename, f):
210 self.idxnames = [self.name]
211 self.map = mmap_read(f)
212 assert(str(self.map[0:8]) == '\377tOc\0\0\0\2')
213 self.fanout = list(struct.unpack('!256I',
214 str(buffer(self.map, 8, 256*4))))
215 self.fanout.append(0) # entry "-1"
216 nsha = self.fanout[255]
217 self.shatable = buffer(self.map, 8 + 256*4, nsha*20)
218 self.ofstable = buffer(self.map,
219 8 + 256*4 + nsha*20 + nsha*4,
221 self.ofs64table = buffer(self.map,
222 8 + 256*4 + nsha*20 + nsha*4 + nsha*4)
224 def _ofs_from_idx(self, idx):
225 ofs = struct.unpack('!I', str(buffer(self.ofstable, idx*4, 4)))[0]
227 idx64 = ofs & 0x7fffffff
228 ofs = struct.unpack('!Q',
229 str(buffer(self.ofs64table, idx64*8, 8)))[0]
232 def _idx_to_hash(self, idx):
233 return str(self.shatable[idx*20:(idx+1)*20])
236 for i in xrange(self.fanout[255]):
237 yield buffer(self.map, 8 + 256*4 + 20*i, 20)
240 extract_bits = _helpers.extract_bits
244 """Wrapper which contains data from multiple index files.
245 Multiple index (.midx) files constitute a wrapper around index (.idx) files
246 and make it possible for bup to expand Git's indexing capabilities to vast
249 def __init__(self, filename):
251 self.force_keep = False
252 assert(filename.endswith('.midx'))
253 self.map = mmap_read(open(filename))
254 if str(self.map[0:4]) != 'MIDX':
255 log('Warning: skipping: invalid MIDX header in %r\n' % filename)
256 self.force_keep = True
257 return self._init_failed()
258 ver = struct.unpack('!I', self.map[4:8])[0]
259 if ver < MIDX_VERSION:
260 log('Warning: ignoring old-style (v%d) midx %r\n'
262 self.force_keep = False # old stuff is boring
263 return self._init_failed()
264 if ver > MIDX_VERSION:
265 log('Warning: ignoring too-new (v%d) midx %r\n'
267 self.force_keep = True # new stuff is exciting
268 return self._init_failed()
270 self.bits = _helpers.firstword(self.map[8:12])
271 self.entries = 2**self.bits
272 self.fanout = buffer(self.map, 12, self.entries*4)
273 shaofs = 12 + self.entries*4
274 nsha = self._fanget(self.entries-1)
275 self.shalist = buffer(self.map, shaofs, nsha*20)
276 self.idxnames = str(self.map[shaofs + 20*nsha:]).split('\0')
278 def _init_failed(self):
281 self.fanout = buffer('\0\0\0\0')
282 self.shalist = buffer('\0'*20)
285 def _fanget(self, i):
287 s = self.fanout[start:start+4]
288 return _helpers.firstword(s)
291 return str(self.shalist[i*20:(i+1)*20])
293 def exists(self, hash):
294 """Return nonempty if the object exists in the index files."""
295 global _total_searches, _total_steps
298 el = extract_bits(want, self.bits)
300 start = self._fanget(el-1)
301 startv = el << (32-self.bits)
305 end = self._fanget(el)
306 endv = (el+1) << (32-self.bits)
307 _total_steps += 1 # lookup table is a step
308 hashv = _helpers.firstword(hash)
309 #print '(%08x) %08x %08x %08x' % (extract_bits(want, 32), startv, hashv, endv)
312 #print '! %08x %08x %08x %d - %d' % (startv, hashv, endv, start, end)
313 mid = start + (hashv-startv)*(end-start-1)/(endv-startv)
314 #print ' %08x %08x %08x %d %d %d' % (startv, hashv, endv, start, mid, end)
316 #print ' %08x' % self._num(v)
319 startv = _helpers.firstword(v)
322 endv = _helpers.firstword(v)
328 for i in xrange(self._fanget(self.entries-1)):
329 yield buffer(self.shalist, i*20, 20)
332 return int(self._fanget(self.entries-1))
337 def __init__(self, dir):
339 assert(_mpi_count == 0) # these things suck tons of VM; don't waste it
349 assert(_mpi_count == 0)
352 return iter(idxmerge(self.packs))
355 return sum(len(pack) for pack in self.packs)
357 def exists(self, hash):
358 """Return nonempty if the object exists in the index files."""
359 global _total_searches
361 if hash in self.also:
363 for i in range(len(self.packs)):
365 _total_searches -= 1 # will be incremented by sub-pack
367 # reorder so most recently used packs are searched first
368 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 skip_midx = skip_midx or ignore_midx
385 d = dict((p.name, p) for p in self.packs
386 if not skip_midx or not isinstance(p, PackMidx))
387 if os.path.exists(self.dir):
390 for ix in self.packs:
391 if isinstance(ix, PackMidx):
392 for name in ix.idxnames:
393 d[os.path.join(self.dir, name)] = ix
394 for f in os.listdir(self.dir):
395 full = os.path.join(self.dir, f)
396 if f.endswith('.midx') and not d.get(full):
398 (mxd, mxf) = os.path.split(mx.name)
400 for n in mx.idxnames:
401 if not os.path.exists(os.path.join(mxd, n)):
402 log(('warning: index %s missing\n' +
403 ' used by %s\n') % (n, mxf))
410 midxl.sort(lambda x,y: -cmp(len(x),len(y)))
413 for sub in ix.idxnames:
414 found = d.get(os.path.join(self.dir, sub))
415 if not found or isinstance(found, PackIdx):
416 # doesn't exist, or exists but not in a midx
418 for name in ix.idxnames:
419 d[os.path.join(self.dir, name)] = ix
422 if not any and not ix.force_keep:
423 debug1('midx: removing redundant: %s\n'
424 % os.path.basename(ix.name))
426 for f in os.listdir(self.dir):
427 full = os.path.join(self.dir, f)
428 if f.endswith('.idx') and not d.get(full):
435 self.packs = list(set(d.values()))
436 debug1('PackIdxList: using %d index%s.\n'
437 % (len(self.packs), len(self.packs)!=1 and 'es' or ''))
439 def packname_containing(self, hash):
440 # figure out which pack contains a given hash.
441 # FIXME: if the midx file format would just *store* this information,
442 # we could calculate it a lot more efficiently. But it's not needed
443 # often, so let's do it like this.
444 for f in os.listdir(self.dir):
445 if f.endswith('.idx'):
446 full = os.path.join(self.dir, f)
456 """Insert an additional object in the list."""
460 """Remove all additional objects from the list."""
464 def calc_hash(type, content):
465 """Calculate some content's hash in the Git fashion."""
466 header = '%s %d\0' % (type, len(content))
472 def _shalist_sort_key(ent):
473 (mode, name, id) = ent
474 if stat.S_ISDIR(int(mode, 8)):
480 def open_idx(filename):
481 if filename.endswith('.idx'):
482 f = open(filename, 'rb')
484 if header[0:4] == '\377tOc':
485 version = struct.unpack('!I', header[4:8])[0]
487 return PackIdxV2(filename, f)
489 raise GitError('%s: expected idx file version 2, got %d'
490 % (filename, version))
491 elif len(header) == 8 and header[0:4] < '\377tOc':
492 return PackIdxV1(filename, f)
494 raise GitError('%s: unrecognized idx file header' % filename)
495 elif filename.endswith('.midx'):
496 return PackMidx(filename)
498 raise GitError('idx filenames must end with .idx or .midx')
501 def idxmerge(idxlist, final_progress=True):
502 """Generate a list of all the objects reachable in a PackIdxList."""
503 total = sum(len(i) for i in idxlist)
504 iters = (iter(i) for i in idxlist)
505 heap = [(next(it), it) for it in iters]
510 if (count % 10024) == 0:
511 progress('Reading indexes: %.2f%% (%d/%d)\r'
512 % (count*100.0/total, count, total))
520 heapq.heapreplace(heap, (e, it))
524 log('Reading indexes: %.2f%% (%d/%d), done.\n' % (100, total, total))
527 def _make_objcache():
528 return PackIdxList(repo('objects/pack'))
531 """Writes Git objects insid a pack file."""
532 def __init__(self, objcache_maker=_make_objcache):
538 self.objcache_maker = objcache_maker
546 (fd,name) = tempfile.mkstemp(suffix='.pack', dir=repo('objects'))
547 self.file = os.fdopen(fd, 'w+b')
548 assert(name.endswith('.pack'))
549 self.filename = name[:-5]
550 self.file.write('PACK\0\0\0\2\0\0\0\0')
551 self.idx = list(list() for i in xrange(256))
553 # the 'sha' parameter is used in client.py's _raw_write(), but not needed
554 # in this basic version.
555 def _raw_write(self, datalist, sha):
558 # in case we get interrupted (eg. KeyboardInterrupt), it's best if
559 # the file never has a *partial* blob. So let's make sure it's
560 # all-or-nothing. (The blob shouldn't be very big anyway, thanks
561 # to our hashsplit algorithm.) f.write() does its own buffering,
562 # but that's okay because we'll flush it in _end().
563 oneblob = ''.join(datalist)
567 raise GitError, e, sys.exc_info()[2]
569 crc = zlib.crc32(oneblob) & 0xffffffff
570 self._update_idx(sha, crc, nw)
575 def _update_idx(self, sha, crc, size):
578 self.idx[ord(sha[0])].append((sha, crc, self.file.tell() - size))
580 def _write(self, sha, type, content):
584 sha = calc_hash(type, content)
585 size, crc = self._raw_write(_encode_packobj(type, content), sha=sha)
588 def breakpoint(self):
589 """Clear byte and object counts and return the last processed id."""
591 self.outbytes = self.count = 0
594 def write(self, type, content):
595 """Write an object in this pack file."""
596 return self._write(calc_hash(type, content), type, content)
598 def _require_objcache(self):
599 if self.objcache is None and self.objcache_maker:
600 self.objcache = self.objcache_maker()
601 if self.objcache is None:
603 "PackWriter not opened or can't check exists w/o objcache")
605 def exists(self, id):
606 """Return non-empty if an object is found in the object cache."""
607 self._require_objcache()
608 return self.objcache.exists(id)
610 def maybe_write(self, type, content):
611 """Write an object to the pack file if not present and return its id."""
612 self._require_objcache()
613 sha = calc_hash(type, content)
614 if not self.exists(sha):
615 self._write(sha, type, content)
616 self.objcache.add(sha)
619 def new_blob(self, blob):
620 """Create a blob object in the pack with the supplied content."""
621 return self.maybe_write('blob', blob)
623 def new_tree(self, shalist):
624 """Create a tree object in the pack."""
625 shalist = sorted(shalist, key = _shalist_sort_key)
627 for (mode,name,bin) in shalist:
630 assert(mode[0] != '0')
632 assert(len(bin) == 20)
633 l.append('%s %s\0%s' % (mode,name,bin))
634 return self.maybe_write('tree', ''.join(l))
636 def _new_commit(self, tree, parent, author, adate, committer, cdate, msg):
638 if tree: l.append('tree %s' % tree.encode('hex'))
639 if parent: l.append('parent %s' % parent.encode('hex'))
640 if author: l.append('author %s %s' % (author, _git_date(adate)))
641 if committer: l.append('committer %s %s' % (committer, _git_date(cdate)))
644 return self.maybe_write('commit', '\n'.join(l))
646 def new_commit(self, parent, tree, date, msg):
647 """Create a commit object in the pack."""
648 userline = '%s <%s@%s>' % (userfullname(), username(), hostname())
649 commit = self._new_commit(tree, parent,
650 userline, date, userline, date,
655 """Remove the pack file from disk."""
661 os.unlink(self.filename + '.pack')
663 def _end(self, run_midx=True):
665 if not f: return None
671 # update object count
673 cp = struct.pack('!i', self.count)
677 # calculate the pack sha1sum
680 for b in chunkyreader(f):
682 packbin = sum.digest()
686 idx_f = open(self.filename + '.idx', 'wb')
687 obj_list_sha = self._write_pack_idx_v2(idx_f, idx, packbin)
690 nameprefix = repo('objects/pack/pack-%s' % obj_list_sha)
691 if os.path.exists(self.filename + '.map'):
692 os.unlink(self.filename + '.map')
693 os.rename(self.filename + '.pack', nameprefix + '.pack')
694 os.rename(self.filename + '.idx', nameprefix + '.idx')
697 auto_midx(repo('objects/pack'))
700 def close(self, run_midx=True):
701 """Close the pack file and move it to its definitive path."""
702 return self._end(run_midx=run_midx)
704 def _write_pack_idx_v2(self, file, idx, packbin):
711 write('\377tOc\0\0\0\2')
716 write(struct.pack('!i', n))
717 part.sort(key=lambda x: x[0])
719 obj_list_sum = Sha1()
723 obj_list_sum.update(entry[0])
726 write(struct.pack('!I', entry[1]))
730 if entry[2] & 0x80000000:
731 write(struct.pack('!I', 0x80000000 | len(ofs64_list)))
732 ofs64_list.append(struct.pack('!Q', entry[2]))
734 write(struct.pack('!i', entry[2]))
735 for ofs64 in ofs64_list:
739 file.write(sum.digest())
740 return obj_list_sum.hexdigest()
744 return '%d %s' % (date, time.strftime('%z', time.localtime(date)))
748 os.environ['GIT_DIR'] = os.path.abspath(repo())
751 def list_refs(refname = None):
752 """Generate a list of tuples in the form (refname,hash).
753 If a ref name is specified, list only this particular ref.
755 argv = ['git', 'show-ref', '--']
758 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
759 out = p.stdout.read().strip()
760 rv = p.wait() # not fatal
764 for d in out.split('\n'):
765 (sha, name) = d.split(' ', 1)
766 yield (name, sha.decode('hex'))
769 def read_ref(refname):
770 """Get the commit id of the most recent commit made on a given ref."""
771 l = list(list_refs(refname))
779 def rev_list(ref, count=None):
780 """Generate a list of reachable commits in reverse chronological order.
782 This generator walks through commits, from child to parent, that are
783 reachable via the specified ref and yields a series of tuples of the form
786 If count is a non-zero integer, limit the number of commits to "count"
789 assert(not ref.startswith('-'))
792 opts += ['-n', str(atoi(count))]
793 argv = ['git', 'rev-list', '--pretty=format:%ct'] + opts + [ref, '--']
794 p = subprocess.Popen(argv, preexec_fn = _gitenv, stdout = subprocess.PIPE)
798 if s.startswith('commit '):
799 commit = s[7:].decode('hex')
803 rv = p.wait() # not fatal
805 raise GitError, 'git rev-list returned error %d' % rv
808 def rev_get_date(ref):
809 """Get the date of the latest commit on the specified ref."""
810 for (date, commit) in rev_list(ref, count=1):
812 raise GitError, 'no such commit %r' % ref
815 def rev_parse(committish):
816 """Resolve the full hash for 'committish', if it exists.
818 Should be roughly equivalent to 'git rev-parse'.
820 Returns the hex value of the hash if it is found, None if 'committish' does
821 not correspond to anything.
823 head = read_ref(committish)
825 debug2("resolved from ref: commit = %s\n" % head.encode('hex'))
828 pL = PackIdxList(repo('objects/pack'))
830 if len(committish) == 40:
832 hash = committish.decode('hex')
842 def update_ref(refname, newval, oldval):
843 """Change the commit pointed to by a branch."""
846 assert(refname.startswith('refs/heads/'))
847 p = subprocess.Popen(['git', 'update-ref', refname,
848 newval.encode('hex'), oldval.encode('hex')],
849 preexec_fn = _gitenv)
850 _git_wait('git update-ref', p)
853 def guess_repo(path=None):
854 """Set the path value in the global variable "repodir".
855 This makes bup look for an existing bup repository, but not fail if a
856 repository doesn't exist. Usually, if you are interacting with a bup
857 repository, you would not be calling this function but using
864 repodir = os.environ.get('BUP_DIR')
866 repodir = os.path.expanduser('~/.bup')
869 def init_repo(path=None):
870 """Create the Git bare repository for bup in a given path."""
872 d = repo() # appends a / to the path
873 parent = os.path.dirname(os.path.dirname(d))
874 if parent and not os.path.exists(parent):
875 raise GitError('parent directory "%s" does not exist\n' % parent)
876 if os.path.exists(d) and not os.path.isdir(os.path.join(d, '.')):
877 raise GitError('"%d" exists but is not a directory\n' % d)
878 p = subprocess.Popen(['git', '--bare', 'init'], stdout=sys.stderr,
879 preexec_fn = _gitenv)
880 _git_wait('git init', p)
881 # Force the index version configuration in order to ensure bup works
882 # regardless of the version of the installed Git binary.
883 p = subprocess.Popen(['git', 'config', 'pack.indexVersion', '2'],
884 stdout=sys.stderr, preexec_fn = _gitenv)
885 _git_wait('git config', p)
888 def check_repo_or_die(path=None):
889 """Make sure a bup repository exists, and abort if not.
890 If the path to a particular repository was not specified, this function
891 initializes the default repository automatically.
894 if not os.path.isdir(repo('objects/pack/.')):
895 if repodir == home_repodir:
898 log('error: %r is not a bup/git repository\n' % repo())
903 """Generate a list of (mode, name, hash) tuples of objects from 'buf'."""
905 while ofs < len(buf):
906 z = buf[ofs:].find('\0')
908 spl = buf[ofs:ofs+z].split(' ', 1)
909 assert(len(spl) == 2)
910 sha = buf[ofs+z+1:ofs+z+1+20]
912 yield (spl[0], spl[1], sha)
917 """Get Git's version and ensure a usable version is installed.
919 The returned version is formatted as an ordered tuple with each position
920 representing a digit in the version tag. For example, the following tuple
921 would represent version 1.6.6.9:
927 p = subprocess.Popen(['git', '--version'],
928 stdout=subprocess.PIPE)
929 gvs = p.stdout.read()
930 _git_wait('git --version', p)
931 m = re.match(r'git version (\S+.\S+)', gvs)
933 raise GitError('git --version weird output: %r' % gvs)
934 _ver = tuple(m.group(1).split('.'))
935 needed = ('1','5', '3', '1')
937 raise GitError('git version %s or higher is required; you have %s'
938 % ('.'.join(needed), '.'.join(_ver)))
942 def _git_wait(cmd, p):
945 raise GitError('%s returned %d' % (cmd, rv))
948 def _git_capture(argv):
949 p = subprocess.Popen(argv, stdout=subprocess.PIPE, preexec_fn = _gitenv)
951 _git_wait(repr(argv), p)
955 class _AbortableIter:
956 def __init__(self, it, onabort = None):
958 self.onabort = onabort
966 return self.it.next()
967 except StopIteration, e:
975 """Abort iteration and call the abortion callback, if needed."""
987 """Link to 'git cat-file' that is used to retrieve blob data."""
990 wanted = ('1','5','6')
993 log('warning: git version < %s; bup will be slow.\n'
996 self.get = self._slow_get
998 self.p = self.inprogress = None
999 self.get = self._fast_get
1003 self.p.stdout.close()
1004 self.p.stdin.close()
1006 self.inprogress = None
1010 self.p = subprocess.Popen(['git', 'cat-file', '--batch'],
1011 stdin=subprocess.PIPE,
1012 stdout=subprocess.PIPE,
1015 preexec_fn = _gitenv)
1017 def _fast_get(self, id):
1018 if not self.p or self.p.poll() != None:
1021 assert(self.p.poll() == None)
1023 log('_fast_get: opening %r while %r is open'
1024 % (id, self.inprogress))
1025 assert(not self.inprogress)
1026 assert(id.find('\n') < 0)
1027 assert(id.find('\r') < 0)
1028 assert(not id.startswith('-'))
1029 self.inprogress = id
1030 self.p.stdin.write('%s\n' % id)
1031 self.p.stdin.flush()
1032 hdr = self.p.stdout.readline()
1033 if hdr.endswith(' missing\n'):
1034 self.inprogress = None
1035 raise KeyError('blob %r is missing' % id)
1036 spl = hdr.split(' ')
1037 if len(spl) != 3 or len(spl[0]) != 40:
1038 raise GitError('expected blob, got %r' % spl)
1039 (hex, type, size) = spl
1041 it = _AbortableIter(chunkyreader(self.p.stdout, int(spl[2])),
1042 onabort = self._abort)
1047 assert(self.p.stdout.readline() == '\n')
1048 self.inprogress = None
1049 except Exception, e:
1053 def _slow_get(self, id):
1054 assert(id.find('\n') < 0)
1055 assert(id.find('\r') < 0)
1056 assert(id[0] != '-')
1057 type = _git_capture(['git', 'cat-file', '-t', id]).strip()
1060 p = subprocess.Popen(['git', 'cat-file', type, id],
1061 stdout=subprocess.PIPE,
1062 preexec_fn = _gitenv)
1063 for blob in chunkyreader(p.stdout):
1065 _git_wait('git cat-file', p)
1067 def _join(self, it):
1072 elif type == 'tree':
1073 treefile = ''.join(it)
1074 for (mode, name, sha) in treeparse(treefile):
1075 for blob in self.join(sha.encode('hex')):
1077 elif type == 'commit':
1078 treeline = ''.join(it).split('\n')[0]
1079 assert(treeline.startswith('tree '))
1080 for blob in self.join(treeline[5:]):
1083 raise GitError('invalid object type %r: expected blob/tree/commit'
1087 """Generate a list of the content of all blobs that can be reached
1088 from an object. The hash given in 'id' must point to a blob, a tree
1089 or a commit. The content of all blobs that can be seen from trees or
1090 commits will be added to the list.
1093 for d in self._join(self.get(id)):
1095 except StopIteration:
1099 """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
1101 for (n,c) in list_refs():
1102 if n.startswith('refs/tags/'):
1107 tags[c].append(name) # more than one tag can point at 'c'