- return self.sub(first)
-
- # walk into a given sub-path of this node. If the last element is
- # a symlink, leave it as a symlink, don't resolve it. (like lstat())
- def lresolve(self, path, stay_inside_fs=False):
- start = self
- if not path:
- return start
- if path.startswith('/'):
- if stay_inside_fs:
- start = self.fs_top()
- else:
- start = self.top()
- path = path[1:]
- parts = re.split(r'/+', path or '.')
- if not parts[-1]:
- parts[-1] = '.'
- #log('parts: %r %r\n' % (path, parts))
- return start._lresolve(parts)
-
- # walk into the given sub-path of this node, and dereference it if it
- # was a symlink.
- def resolve(self, path = ''):
- return self.lresolve(path).lresolve('.')
-
- # like resolve(), but don't worry if the last symlink points at an
- # invalid path.
- # (still returns an error if any intermediate nodes were invalid)
- def try_resolve(self, path = ''):
- n = self.lresolve(path)
- try:
- n = n.lresolve('.')
- except NoSuchFile:
- pass
- return n
-
- def nlinks(self):
- if self._subs == None:
- self._mksubs()
- return 1
-
- def size(self):
- return 0
-
- def open(self):
- raise NotFile('%s is not a regular file' % self.name)
-
-
-class File(Node):
- def __init__(self, parent, name, mode, hash, bupmode):
- Node.__init__(self, parent, name, mode, hash)
- self.bupmode = bupmode
- self._cached_size = None
- self._filereader = None
-
- def open(self):
- # You'd think FUSE might call this only once each time a file is
- # opened, but no; it's really more of a refcount, and it's called
- # once per read(). Thus, it's important to cache the filereader
- # object here so we're not constantly re-seeking.
- if not self._filereader:
- self._filereader = _FileReader(self.hash, self.size(),
- self.bupmode == git.BUP_CHUNKED)
- self._filereader.seek(0)
- return self._filereader
-
- def size(self):
- if self._cached_size == None:
- log('<<<<File.size() is calculating...\n')
- if self.bupmode == git.BUP_CHUNKED:
- self._cached_size = _total_size(self.hash)
- else:
- self._cached_size = _chunk_len(self.hash)
- log('<<<<File.size() done.\n')
- return self._cached_size
-
-
-_symrefs = 0
-class Symlink(File):
- def __init__(self, parent, name, hash, bupmode):
- File.__init__(self, parent, name, 0120000, hash, bupmode)
-
- def size(self):
- return len(self.readlink())
-
- def readlink(self):
- return ''.join(cp().join(self.hash.encode('hex')))
-
- def dereference(self):
- global _symrefs
- if _symrefs > 100:
- raise TooManySymlinks('too many levels of symlinks: %r'
- % self.fullname())
- _symrefs += 1
- try:
- return self.parent.lresolve(self.readlink(),
- stay_inside_fs=True)
- except NoSuchFile:
- raise NoSuchFile("%s: broken symlink to %r"
- % (self.fullname(), self.readlink()))
- finally:
- _symrefs -= 1
-
- def _lresolve(self, parts):
- return self.dereference()._lresolve(parts)
-
-
-class FakeSymlink(Symlink):
- def __init__(self, parent, name, toname):
- Symlink.__init__(self, parent, name, EMPTY_SHA, git.BUP_NORMAL)
- self.toname = toname
-
- def readlink(self):
- return self.toname
-
-
-class Dir(Node):
- def _mksubs(self):
- self._subs = {}
- it = cp().get(self.hash.encode('hex'))
- type = it.next()
- if type == 'commit':
- del it
- it = cp().get(self.hash.encode('hex') + ':')
- type = it.next()
- assert(type == 'tree')
- for (mode,mangled_name,sha) in git._treeparse(''.join(it)):
- mode = int(mode, 8)
- name = mangled_name
- (name,bupmode) = git.demangle_name(mangled_name)
- if bupmode == git.BUP_CHUNKED:
- mode = 0100644
- if stat.S_ISDIR(mode):
- self._subs[name] = Dir(self, name, mode, sha)
- elif stat.S_ISLNK(mode):
- self._subs[name] = Symlink(self, name, sha, bupmode)
- else:
- self._subs[name] = File(self, name, mode, sha, bupmode)
-
-
-class CommitList(Node):
- def __init__(self, parent, name, hash):
- Node.__init__(self, parent, name, 040000, hash)
-
- def _mksubs(self):
- self._subs = {}
- revs = list(git.rev_list(self.hash.encode('hex')))
- for (date, commit) in revs:
- l = time.localtime(date)
- ls = time.strftime('%Y-%m-%d-%H%M%S', l)
- commithex = '.' + commit.encode('hex')
- n1 = Dir(self, commithex, 040000, commit)
- n2 = FakeSymlink(self, ls, commithex)
- n1.ctime = n1.mtime = n2.ctime = n2.mtime = date
- self._subs[commithex] = n1
- self._subs[ls] = n2
- latest = max(revs)
- if latest:
- (date, commit) = latest
- commithex = '.' + commit.encode('hex')
- n2 = FakeSymlink(self, 'latest', commithex)
- n2.ctime = n2.mtime = date
- self._subs['latest'] = n2
-
-
-class RefList(Node):
- def __init__(self, parent):
- Node.__init__(self, parent, '/', 040000, EMPTY_SHA)
-
- def _mksubs(self):
- self._subs = {}
- for (name,sha) in git.list_refs():
- if name.startswith('refs/heads/'):
- name = name[11:]
- date = git.rev_get_date(sha.encode('hex'))
- n1 = CommitList(self, name, sha)
- n1.ctime = n1.mtime = date
- self._subs[name] = n1
-
+ ndig = len(str(ndup - 1))
+ fmt = b'%s-' + b'%0' + (b'%d' % ndig) + b'd'
+ for i in range(ndup - 1, -1, -1):
+ yield fmt % (name, i)
+
+def parse_rev(f):
+ items = f.readline().split(None)
+ assert len(items) == 2
+ tree, auth_sec = items
+ return unhexlify(tree), int(auth_sec)
+
+def _name_for_rev(rev):
+ commit_oidx, (tree_oid, utc) = rev
+ return strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
+
+def _item_for_rev(rev):
+ commit_oidx, (tree_oid, utc) = rev
+ coid = unhexlify(commit_oidx)
+ item = cache_get_commit_item(coid, need_meta=False)
+ if item:
+ return item
+ item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
+ commit_key = b'itm:' + coid
+ cache_notice(commit_key, item)
+ return item
+
+# non-string singleton
+_HAS_META_ENTRY = object()
+
+def cache_commit(repo, oid, require_meta=True):
+ """Build, cache, and return a "name -> commit_item" dict of the entire
+ commit rev-list.
+
+ """
+ entries = {}
+ entries[b'.'] = _revlist_item_from_oid(repo, oid, require_meta)
+ revs = repo.rev_list((hexlify(oid),), format=b'%T %at',
+ parse=parse_rev)
+ rev_items, rev_names = tee(revs)
+ revs = None # Don't disturb the tees
+ rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
+ rev_items = (_item_for_rev(x) for x in rev_items)
+ tip = None
+ for item in rev_items:
+ name = next(rev_names)
+ tip = tip or (name, item)
+ entries[name] = item
+ entries[b'latest'] = FakeLink(meta=default_symlink_mode, target=tip[0])
+ revlist_key = b'rvl:' + tip[1].coid
+ entries[_HAS_META_ENTRY] = require_meta
+ cache_notice(revlist_key, entries, overwrite=True)
+ return entries
+
+def revlist_items(repo, oid, names, require_meta=True):
+ assert len(oid) == 20
+
+ # Special case '.' instead of caching the whole history since it's
+ # the only way to get the metadata for the commit.
+ if names and all(x == b'.' for x in names):
+ yield b'.', _revlist_item_from_oid(repo, oid, require_meta)
+ return
+
+ # For now, don't worry about the possibility of the contents being
+ # "too big" for the cache.
+ revlist_key = b'rvl:' + oid
+ entries = cache_get(revlist_key)
+ if entries and require_meta and not entries[_HAS_META_ENTRY]:
+ entries = None
+ if not entries:
+ entries = cache_commit(repo, oid, require_meta)
+
+ if not names:
+ for name in sorted((n for n in entries.keys() if n != _HAS_META_ENTRY)):
+ yield name, entries[name]
+ return
+
+ names = frozenset(name for name in names
+ if _save_name_rx.match(name) or name in (b'.', b'latest'))
+
+ if b'.' in names:
+ yield b'.', entries[b'.']
+ for name in (n for n in names if n != b'.'):
+ if name == _HAS_META_ENTRY:
+ continue
+ commit = entries.get(name)
+ if commit:
+ yield name, commit
+
+def tags_items(repo, names):
+ global _tags
+
+ def tag_item(oid):
+ assert len(oid) == 20
+ oidx = hexlify(oid)
+ it = repo.cat(oidx)
+ _, typ, size = next(it)
+ if typ == b'commit':
+ return cache_get_commit_item(oid, need_meta=False) \
+ or _commit_item_from_data(oid, b''.join(it))
+ for _ in it: pass
+ if typ == b'blob':
+ return Item(meta=default_file_mode, oid=oid)
+ elif typ == b'tree':
+ return Item(meta=default_dir_mode, oid=oid)
+ raise Exception('unexpected tag type ' + typ.decode('ascii')
+ + ' for tag ' + path_msg(name))
+
+ if not names:
+ yield b'.', _tags
+ # We have to pull these all into ram because tag_item calls cat()
+ for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
+ assert(name.startswith(b'refs/tags/'))
+ name = name[10:]
+ yield name, tag_item(oid)
+ return
+
+ # Assumes no duplicate refs
+ if isinstance(names, (frozenset, set)):
+ names = frozenset(names)
+ remaining = len(names)
+ last_name = max(names)
+ if b'.' in names:
+ yield b'.', _tags
+ if remaining == 1:
+ return
+ remaining -= 1
+
+ for name, oid in repo.refs(names, limit_to_tags=True):
+ assert(name.startswith(b'refs/tags/'))
+ name = name[10:]
+ if name > last_name:
+ return
+ if name not in names:
+ continue
+ yield name, tag_item(oid)
+ if remaining == 1:
+ return
+ remaining -= 1
+
+def contents(repo, item, names=None, want_meta=True):
+ """Yields information about the items contained in item. Yields
+ (name, item) for each name in names, if the name exists, in an
+ unspecified order. If there are no names, then yields (name,
+ item) for all items, including, a first item named '.'
+ representing the container itself.
+
+ The meta value for any directories other than '.' will be a
+ default directory mode, not a Metadata object. This is because
+ the actual metadata for a directory is stored inside the directory
+ (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
+
+ Note that want_meta is advisory. For any given item, item.meta
+ might be a Metadata instance or a mode, and if the former,
+ meta.size might be None. Missing sizes can be computed via via
+ item_size() or augment_item_meta(..., include_size=True).
+
+ Do not modify any item.meta Metadata instances directly. If
+ needed, make a copy via item.meta.copy() and modify that instead.
+
+ """
+ # Q: are we comfortable promising '.' first when no names?
+ global _root, _tags
+ assert repo
+ assert S_ISDIR(item_mode(item))
+ if isinstance(item, real_tree_types):
+ it = repo.cat(hexlify(item.oid))
+ _, obj_t, size = next(it)
+ data = b''.join(it)
+ if obj_t != b'tree':
+ for _ in it: pass
+ # Note: it shouldn't be possible to see an Item with type
+ # 'commit' since a 'commit' should always produce a Commit.
+ raise Exception('unexpected git ' + obj_t.decode('ascii'))
+ if want_meta:
+ item_gen = tree_items_with_meta(repo, item.oid, data, names)
+ else:
+ item_gen = tree_items(item.oid, data, names)
+ elif isinstance(item, RevList):
+ item_gen = revlist_items(repo, item.oid, names,
+ require_meta=want_meta)
+ elif isinstance(item, Root):
+ item_gen = root_items(repo, names, want_meta)
+ elif isinstance(item, Tags):
+ item_gen = tags_items(repo, names)
+ else:
+ raise Exception('unexpected VFS item ' + str(item))
+ for x in item_gen:
+ yield x
+
+def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
+ cache_key = b'res:%d%d%d:%s\0%s' \
+ % (bool(want_meta), bool(follow), repo.id(),
+ (b'/'.join(x[0] for x in parent) if parent else b''),
+ path)
+ resolution = cache_get(cache_key)
+ if resolution:
+ return resolution
+
+ def notice_resolution(r):
+ cache_notice(cache_key, r)
+ return r
+
+ def raise_dir_required_but_not_dir(path, parent, past):
+ raise IOError(ENOTDIR,
+ "path %s%s resolves to non-directory %r"
+ % (path,
+ ' (relative to %r)' % parent if parent else '',
+ past),
+ terminus=past)
+ global _root
+ assert repo
+ assert len(path)
+ if parent:
+ for x in parent:
+ assert len(x) == 2
+ assert isinstance(x[0], (bytes, str_type))
+ assert isinstance(x[1], item_types)
+ assert parent[0][1] == _root
+ if not S_ISDIR(item_mode(parent[-1][1])):
+ raise IOError(ENOTDIR,
+ 'path resolution parent %r is not a directory'
+ % (parent,))
+ is_absolute, must_be_dir, future = _decompose_path(path)
+ if must_be_dir:
+ follow = True
+ if not future: # path was effectively '.' or '/'
+ if is_absolute:
+ return notice_resolution(((b'', _root),))
+ if parent:
+ return notice_resolution(tuple(parent))
+ return notice_resolution(((b'', _root),))
+ if is_absolute:
+ past = [(b'', _root)]
+ else:
+ past = list(parent) if parent else [(b'', _root)]
+ hops = 0
+ while True:
+ if not future:
+ if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
+ raise_dir_required_but_not_dir(path, parent, past)
+ return notice_resolution(tuple(past))
+ segment = future.pop()
+ if segment == b'..':
+ assert len(past) > 0
+ if len(past) > 1: # .. from / is /
+ assert S_ISDIR(item_mode(past[-1][1]))
+ past.pop()
+ else:
+ parent_name, parent_item = past[-1]
+ wanted = (segment,) if not want_meta else (b'.', segment)
+ items = tuple(contents(repo, parent_item, names=wanted,
+ want_meta=want_meta))
+ if not want_meta:
+ item = items[0][1] if items else None
+ else: # First item will be '.' and have the metadata
+ item = items[1][1] if len(items) == 2 else None
+ dot, dot_item = items[0]
+ assert dot == b'.'
+ past[-1] = parent_name, parent_item
+ if not item:
+ past.append((segment, None),)
+ return notice_resolution(tuple(past))
+ mode = item_mode(item)
+ if not S_ISLNK(mode):
+ if not S_ISDIR(mode):
+ past.append((segment, item),)
+ if future:
+ raise IOError(ENOTDIR,
+ 'path %r%s ends internally in non-directory here: %r'
+ % (path,
+ ' (relative to %r)' % parent if parent else '',
+ past),
+ terminus=past)
+ if must_be_dir:
+ raise_dir_required_but_not_dir(path, parent, past)
+ return notice_resolution(tuple(past))
+ # It's treeish
+ if want_meta and isinstance(item, real_tree_types):
+ dir_meta = _find_treeish_oid_metadata(repo, item.oid)
+ if dir_meta:
+ item = item._replace(meta=dir_meta)
+ past.append((segment, item))
+ else: # symlink
+ if not future and not follow:
+ past.append((segment, item),)
+ continue
+ if hops > 100:
+ raise IOError(ELOOP,
+ 'too many symlinks encountered while resolving %r%s'
+ % (path, ' relative to %r' % parent if parent else ''),
+ terminus=tuple(past + [(segment, item)]))
+ target = readlink(repo, item)
+ is_absolute, _, target_future = _decompose_path(target)
+ if is_absolute:
+ if not target_future: # path was effectively '/'
+ return notice_resolution(((b'', _root),))
+ past = [(b'', _root)]
+ future = target_future
+ else:
+ future.extend(target_future)
+ hops += 1
+
+def resolve(repo, path, parent=None, want_meta=True, follow=True):
+ """Follow the path in the virtual filesystem and return a tuple
+ representing the location, if any, denoted by the path. Each
+ element in the result tuple will be (name, info), where info will
+ be a VFS item that can be passed to functions like item_mode().
+
+ If follow is false, and if the final path element is a symbolic
+ link, don't follow it, just return it in the result.
+
+ If a path segment that does not exist is encountered during
+ resolution, the result will represent the location of the missing
+ item, and that item in the result will be None.
+
+ Any attempt to traverse a non-directory will raise a VFS ENOTDIR
+ IOError exception.
+
+ Any symlinks along the path, including at the end, will be
+ resolved. A VFS IOError with the errno attribute set to ELOOP
+ will be raised if too many symlinks are traversed while following
+ the path. That exception is effectively like a normal
+ ELOOP IOError exception, but will include a terminus element
+ describing the location of the failure, which will be a tuple of
+ (name, info) elements.
+
+ The parent, if specified, must be a sequence of (name, item)
+ tuples, and will provide the starting point for the resolution of
+ the path. If no parent is specified, resolution will start at
+ '/'.
+
+ The result may include elements of parent directly, so they must
+ not be modified later. If this is a concern, pass in "name,
+ copy_item(item) for name, item in parent" instead.
+
+ When want_meta is true, detailed metadata will be included in each
+ result item if it's avaiable, otherwise item.meta will be an
+ integer mode. The metadata size may or may not be provided, but
+ can be computed by item_size() or augment_item_meta(...,
+ include_size=True). Setting want_meta=False is rarely desirable
+ since it can limit the VFS to just the metadata git itself can
+ represent, and so, as an example, fifos and sockets will appear to
+ be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
+ But the option is provided because it may be more efficient when
+ only the path names or the more limited metadata is sufficient.
+
+ Do not modify any item.meta Metadata instances directly. If
+ needed, make a copy via item.meta.copy() and modify that instead.
+
+ """
+ if repo.is_remote():
+ # Redirect to the more efficient remote version
+ return repo.resolve(path, parent=parent, want_meta=want_meta,
+ follow=follow)
+ result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
+ follow=follow)
+ _, leaf_item = result[-1]
+ if leaf_item and follow:
+ assert not S_ISLNK(item_mode(leaf_item))
+ return result
+
+def try_resolve(repo, path, parent=None, want_meta=True):
+ """If path does not refer to a symlink, does not exist, or refers to a
+ valid symlink, behave exactly like resolve(..., follow=True). If
+ path refers to an invalid symlink, behave like resolve(...,
+ follow=False).
+
+ """
+ res = resolve(repo, path, parent=parent, want_meta=want_meta, follow=False)
+ leaf_name, leaf_item = res[-1]
+ if not leaf_item:
+ return res
+ if not S_ISLNK(item_mode(leaf_item)):
+ return res
+ follow = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
+ follow_name, follow_item = follow[-1]
+ if follow_item:
+ return follow
+ return res
+
+def augment_item_meta(repo, item, include_size=False):
+ """Ensure item has a Metadata instance for item.meta. If item.meta is
+ currently a mode, replace it with a compatible "fake" Metadata
+ instance. If include_size is true, ensure item.meta.size is
+ correct, computing it if needed. If item.meta is a Metadata
+ instance, this call may modify it in place or replace it.
+
+ """
+ # If we actually had parallelism, we'd need locking...
+ assert repo
+ m = item.meta
+ if isinstance(m, Metadata):
+ if include_size and m.size is None:
+ m.size = _compute_item_size(repo, item)
+ return item._replace(meta=m)
+ return item
+ # m is mode
+ meta = Metadata()
+ meta.mode = m
+ meta.uid = meta.gid = None
+ meta.atime = meta.mtime = meta.ctime = 0
+ if S_ISLNK(m):
+ if isinstance(item, FakeLink):
+ target = item.target
+ else:
+ target = _readlink(repo, item.oid)
+ meta.symlink_target = target
+ meta.size = len(target)
+ elif include_size:
+ meta.size = _compute_item_size(repo, item)
+ return item._replace(meta=meta)
+
+def fill_in_metadata_if_dir(repo, item):
+ """If item is a directory and item.meta is not a Metadata instance,
+ attempt to find the metadata for the directory. If found, return
+ a new item augmented to include that metadata. Otherwise, return
+ item. May be useful for the output of contents().
+
+ """
+ if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
+ items = tuple(contents(repo, item, (b'.',), want_meta=True))
+ assert len(items) == 1
+ assert items[0][0] == b'.'
+ item = items[0][1]
+ return item
+
+def ensure_item_has_metadata(repo, item, include_size=False):
+ """If item is a directory, attempt to find and add its metadata. If
+ the item still doesn't have a Metadata instance for item.meta,
+ give it one via augment_item_meta(). May be useful for the output
+ of contents().