+
+
+_cp = {}
+
+def cp(repo_dir=None):
+ """Create a CatPipe object or reuse the already existing one."""
+ global _cp, repodir
+ if not repo_dir:
+ repo_dir = repodir or repo()
+ repo_dir = os.path.abspath(repo_dir)
+ cp = _cp.get(repo_dir)
+ if not cp:
+ cp = CatPipe(repo_dir)
+ _cp[repo_dir] = cp
+ return cp
+
+
+def tags(repo_dir = None):
+ """Return a dictionary of all tags in the form {hash: [tag_names, ...]}."""
+ tags = {}
+ for n, c in list_refs(repo_dir = repo_dir, limit_to_tags=True):
+ assert(n.startswith('refs/tags/'))
+ name = n[10:]
+ if not c in tags:
+ tags[c] = []
+ tags[c].append(name) # more than one tag can point at 'c'
+ return tags
+
+
+class MissingObject(KeyError):
+ def __init__(self, oid):
+ self.oid = oid
+ KeyError.__init__(self, 'object %r is missing' % oid.encode('hex'))
+
+
+WalkItem = namedtuple('WalkItem', ['oid', 'type', 'mode',
+ 'path', 'chunk_path', 'data'])
+# The path is the mangled path, and if an item represents a fragment
+# of a chunked file, the chunk_path will be the chunked subtree path
+# for the chunk, i.e. ['', '2d3115e', ...]. The top-level path for a
+# chunked file will have a chunk_path of ['']. So some chunk subtree
+# of the file '/foo/bar/baz' might look like this:
+#
+# item.path = ['foo', 'bar', 'baz.bup']
+# item.chunk_path = ['', '2d3115e', '016b097']
+# item.type = 'tree'
+# ...
+
+
+def walk_object(cat_pipe, oidx,
+ stop_at=None,
+ include_data=None):
+ """Yield everything reachable from oidx via cat_pipe as a WalkItem,
+ stopping whenever stop_at(oidx) returns true. Throw MissingObject
+ if a hash encountered is missing from the repository, and don't
+ read or return blob content in the data field unless include_data
+ is set.
+ """
+ # Maintain the pending stack on the heap to avoid stack overflow
+ pending = [(oidx, [], [], None)]
+ while len(pending):
+ oidx, parent_path, chunk_path, mode = pending.pop()
+ oid = oidx.decode('hex')
+ if stop_at and stop_at(oidx):
+ continue
+
+ if (not include_data) and mode and stat.S_ISREG(mode):
+ # If the object is a "regular file", then it's a leaf in
+ # the graph, so we can skip reading the data if the caller
+ # hasn't requested it.
+ yield WalkItem(oid=oid, type='blob',
+ chunk_path=chunk_path, path=parent_path,
+ mode=mode,
+ data=None)
+ continue
+
+ item_it = cat_pipe.get(oidx)
+ get_oidx, typ, _ = next(item_it)
+ if not get_oidx:
+ raise MissingObject(oidx.decode('hex'))
+ if typ not in ('blob', 'commit', 'tree'):
+ raise Exception('unexpected repository object type %r' % typ)
+
+ # FIXME: set the mode based on the type when the mode is None
+ if typ == 'blob' and not include_data:
+ # Dump data until we can ask cat_pipe not to fetch it
+ for ignored in item_it:
+ pass
+ data = None
+ else:
+ data = ''.join(item_it)
+
+ yield WalkItem(oid=oid, type=typ,
+ chunk_path=chunk_path, path=parent_path,
+ mode=mode,
+ data=(data if include_data else None))
+
+ if typ == 'commit':
+ commit_items = parse_commit(data)
+ for pid in commit_items.parents:
+ pending.append((pid, parent_path, chunk_path, mode))
+ pending.append((commit_items.tree, parent_path, chunk_path,
+ hashsplit.GIT_MODE_TREE))
+ elif typ == 'tree':
+ for mode, name, ent_id in tree_decode(data):
+ demangled, bup_type = demangle_name(name, mode)
+ if chunk_path:
+ sub_path = parent_path
+ sub_chunk_path = chunk_path + [name]
+ else:
+ sub_path = parent_path + [name]
+ if bup_type == BUP_CHUNKED:
+ sub_chunk_path = ['']
+ else:
+ sub_chunk_path = chunk_path
+ pending.append((ent_id.encode('hex'), sub_path, sub_chunk_path,
+ mode))