1 """Virtual File System interface to bup repository content.
3 This module provides a path-based interface to the content of a bup
6 The VFS is structured like this:
9 /SAVE-NAME/SAVE-DATE/...
12 Each path is represented by an item that has least an item.meta which
13 may be either a Metadata object, or an integer mode. Functions like
14 item_mode() and item_size() will return the mode and size in either
15 case. Any item.meta Metadata instances must not be modified directly.
16 Make a copy to modify via item.meta.copy() if needed.
18 The want_meta argument is advisory for calls that accept it, and it
19 may not be honored. Callers must be able to handle an item.meta value
20 that is either an instance of Metadata or an integer mode, perhaps
21 via item_mode() or augment_item_meta().
23 Setting want_meta=False is rarely desirable since it can limit the VFS
24 to only the metadata that git itself can represent, and so for
25 example, fifos and sockets will appear to be regular files
26 (e.g. S_ISREG(item_mode(item)) will be true). But the option is still
27 provided because it may be more efficient when just the path names or
28 the more limited metadata is sufficient.
30 Any given metadata object's size may be None, in which case the size
31 can be computed via item_size() or augment_item_meta(...,
34 When traversing a directory using functions like contents(), the meta
35 value for any directories other than '.' will be a default directory
36 mode, not a Metadata object. This is because the actual metadata for
37 a directory is stored inside the directory (see
38 fill_in_metadata_if_dir() or ensure_item_has_metadata()).
40 Commit items represent commits (e.g. /.tag/some-commit or
41 /foo/latest), and for most purposes, they appear as the underlying
42 tree. S_ISDIR(item_mode(item)) will return true for both tree Items
43 and Commits and the commit's oid is the tree hash; the commit hash is
48 from __future__ import absolute_import, print_function
49 from collections import namedtuple
50 from errno import ELOOP, ENOENT, ENOTDIR
51 from itertools import chain, dropwhile, groupby, izip, tee
52 from random import randrange
53 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
54 from time import localtime, strftime
55 import exceptions, re, sys
57 from bup import client, git, metadata
58 from bup.compat import range
59 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
60 from bup.helpers import debug2, last
61 from bup.metadata import Metadata
62 from bup.repo import LocalRepo, RemoteRepo
65 class IOError(exceptions.IOError):
66 def __init__(self, errno, message, terminus=None):
67 exceptions.IOError.__init__(self, errno, message)
68 self.terminus = terminus
70 default_file_mode = S_IFREG | 0o644
71 default_dir_mode = S_IFDIR | 0o755
72 default_symlink_mode = S_IFLNK | 0o755
74 def _default_mode_for_gitmode(gitmode):
76 return default_file_mode
78 return default_dir_mode
80 return default_symlink_mode
81 raise Exception('unexpected git mode ' + oct(gitmode))
83 def _normal_or_chunked_file_size(repo, oid):
84 """Return the size of the normal or chunked file indicated by oid."""
85 # FIXME: --batch-format CatPipe?
86 it = repo.cat(oid.encode('hex'))
87 _, obj_t, size = next(it)
89 while obj_t == 'tree':
90 mode, name, last_oid = last(tree_decode(''.join(it)))
92 it = repo.cat(last_oid.encode('hex'))
93 _, obj_t, size = next(it)
94 return ofs + sum(len(b) for b in it)
96 def _tree_chunks(repo, tree, startofs):
97 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
99 # name is the chunk's hex offset in the original file
100 tree = dropwhile(lambda (_1, name, _2): int(name, 16) < startofs, tree)
101 for mode, name, oid in tree:
103 skipmore = startofs - ofs
106 it = repo.cat(oid.encode('hex'))
107 _, obj_t, size = next(it)
110 assert obj_t == 'tree'
111 for b in _tree_chunks(repo, tree_decode(data), skipmore):
114 assert obj_t == 'blob'
115 yield data[skipmore:]
118 def __init__(self, repo, oid, startofs):
119 it = repo.cat(oid.encode('hex'))
120 _, obj_t, size = next(it)
121 isdir = obj_t == 'tree'
124 self.it = _tree_chunks(repo, tree_decode(data), startofs)
128 self.blob = data[startofs:]
131 def next(self, size):
133 while len(out) < size:
134 if self.it and not self.blob:
136 self.blob = self.it.next()
137 except StopIteration:
140 want = size - len(out)
141 out += self.blob[:want]
142 self.blob = self.blob[want:]
145 debug2('next(%d) returned %d\n' % (size, len(out)))
149 class _FileReader(object):
150 def __init__(self, repo, oid, known_size=None):
151 assert len(oid) == 20
156 self._size = known_size
158 def _compute_size(self):
160 self._size = _normal_or_chunked_file_size(self._repo, self.oid)
165 raise IOError(errno.EINVAL, 'Invalid argument')
166 if ofs > self._compute_size():
167 raise IOError(errno.EINVAL, 'Invalid argument')
173 def read(self, count=-1):
175 count = self._compute_size() - self.ofs
176 if not self.reader or self.reader.ofs != self.ofs:
177 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
179 buf = self.reader.next(count)
182 raise # our offsets will be all screwed up otherwise
191 def __exit__(self, type, value, traceback):
195 _multiple_slashes_rx = re.compile(r'//+')
197 def _decompose_path(path):
198 """Return a boolean indicating whether the path is absolute, and a
199 reversed list of path elements, omitting any occurrences of "."
200 and ignoring any leading or trailing slash. If the path is
201 effectively '/' or '.', return an empty list.
204 path = re.sub(_multiple_slashes_rx, '/', path)
206 return True, True, []
207 is_absolute = must_be_dir = False
208 if path.startswith('/'):
211 for suffix in ('/', '/.'):
212 if path.endswith(suffix):
214 path = path[:-len(suffix)]
215 parts = [x for x in path.split('/') if x != '.']
218 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
219 return is_absolute, must_be_dir, parts
222 Item = namedtuple('Item', ('meta', 'oid'))
223 Chunky = namedtuple('Chunky', ('meta', 'oid'))
224 Root = namedtuple('Root', ('meta'))
225 Tags = namedtuple('Tags', ('meta'))
226 RevList = namedtuple('RevList', ('meta', 'oid'))
227 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
229 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
230 real_tree_types = frozenset((Item, Commit))
232 _root = Root(meta=default_dir_mode)
233 _tags = Tags(meta=default_dir_mode)
238 ### A general purpose shared cache with (currently) cheap random
239 ### eviction. There is currently no weighting so a single commit item
240 ### is just as likely to be evicted as an entire "rev-list". See
241 ### is_valid_cache_key for a description of the expected content.
245 _cache_max_items = 30000
248 global _cache, _cache_keys
252 def is_valid_cache_key(x):
253 """Return logically true if x looks like it could be a valid cache key
254 (with respect to structure). Current valid cache entries:
256 commit_oid + ':r' -> rev-list
257 i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
259 # Suspect we may eventually add "(container_oid, name) -> ...", and others.
264 if len(x) == 22 and x.endswith(b':r'):
269 assert is_valid_cache_key(key)
270 return _cache.get(key)
272 def cache_notice(key, value):
273 global _cache, _cache_keys, _cache_max_items
274 assert is_valid_cache_key(key)
277 if len(_cache) < _cache_max_items:
278 _cache_keys.append(key)
281 victim_i = randrange(0, len(_cache_keys))
282 victim = _cache_keys[victim_i]
284 _cache_keys[victim_i] = key
288 def cache_get_commit_item(oid, need_meta=True):
289 """Return the requested tree item if it can be found in the cache.
290 When need_meta is true don't return a cached item that only has a
292 # tree might be stored independently, or as '.' with its entries.
293 item = cache_get(oid)
297 if isinstance(item.meta, Metadata):
299 entries = cache_get(oid + b':r')
303 def cache_get_revlist_item(oid, need_meta=True):
304 commit = cache_get_commit_item(oid, need_meta=need_meta)
306 return RevList(oid=oid, meta=commit.meta)
310 """Return a completely independent copy of item, such that
311 modifications will not affect the original.
314 meta = getattr(item, 'meta', None)
317 return(item._replace(meta=meta.copy()))
320 """Return the integer mode (stat st_mode) for item."""
322 if isinstance(m, Metadata):
326 def _read_dir_meta(bupm):
327 # This is because save writes unmodified Metadata() entries for
328 # fake parents -- test-save-strip-graft.sh demonstrates.
329 m = Metadata.read(bupm)
331 return default_dir_mode
332 assert m.mode is not None
337 def tree_data_and_bupm(repo, oid):
338 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
339 tree has no metadata (i.e. older bup save, or non-bup tree).
342 assert len(oid) == 20
343 it = repo.cat(oid.encode('hex'))
344 _, item_t, size = next(it)
346 if item_t == 'commit':
347 commit = parse_commit(data)
348 it = repo.cat(commit.tree)
349 _, item_t, size = next(it)
351 assert item_t == 'tree'
352 elif item_t != 'tree':
353 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
354 for _, mangled_name, sub_oid in tree_decode(data):
355 if mangled_name == '.bupm':
357 if mangled_name > '.bupm':
361 def _find_treeish_oid_metadata(repo, oid):
362 """Return the metadata for the tree or commit oid, or None if the tree
363 has no metadata (i.e. older bup save, or non-bup tree).
366 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
368 with _FileReader(repo, bupm_oid) as meta_stream:
369 return _read_dir_meta(meta_stream)
372 def _readlink(repo, oid):
373 return ''.join(repo.join(oid.encode('hex')))
375 def readlink(repo, item):
376 """Return the link target of item, which must be a symlink. Reads the
377 target from the repository if necessary."""
379 assert S_ISLNK(item_mode(item))
380 if isinstance(item.meta, Metadata):
381 target = item.meta.symlink_target
384 return _readlink(repo, item.oid)
386 def _compute_item_size(repo, item):
387 mode = item_mode(item)
389 size = _normal_or_chunked_file_size(repo, item.oid)
392 return len(_readlink(repo, item.oid))
395 def item_size(repo, item):
396 """Return the size of item, computing it if necessary."""
398 if isinstance(m, Metadata) and m.size is not None:
400 return _compute_item_size(repo, item)
402 def tree_data_reader(repo, oid):
403 """Return an open reader for all of the data contained within oid. If
404 oid refers to a tree, recursively concatenate all of its contents."""
405 return _FileReader(repo, oid)
407 def fopen(repo, item):
408 """Return an open reader for the given file item."""
409 assert S_ISREG(item_mode(item))
410 return tree_data_reader(repo, item.oid)
412 def _commit_item_from_data(oid, data):
413 info = parse_commit(data)
414 return Commit(meta=default_dir_mode,
415 oid=info.tree.decode('hex'),
418 def _commit_item_from_oid(repo, oid, require_meta):
419 commit = cache_get_commit_item(oid, need_meta=require_meta)
420 if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
422 it = repo.cat(oid.encode('hex'))
423 _, typ, size = next(it)
424 assert typ == 'commit'
425 commit = _commit_item_from_data(oid, ''.join(it))
427 meta = _find_treeish_oid_metadata(repo, commit.oid)
429 commit = commit._replace(meta=meta)
430 cache_notice(oid, commit)
433 def _revlist_item_from_oid(repo, oid, require_meta):
434 commit = _commit_item_from_oid(repo, oid, require_meta)
435 return RevList(oid=oid, meta=commit.meta)
437 def root_items(repo, names=None, want_meta=True):
438 """Yield (name, item) for the items in '/' in the VFS. Return
439 everything if names is logically false, otherwise return only
440 items with a name in the collection.
443 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
449 # FIXME: maybe eventually support repo.clone() or something
450 # and pass in two repos, so we can drop the tuple() and stream
451 # in parallel (i.e. meta vs refs).
452 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
453 assert(name.startswith('refs/heads/'))
454 yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
462 if ref in ('.', '.tag'):
464 it = repo.cat('refs/heads/' + ref)
465 oidx, typ, size = next(it)
469 assert typ == 'commit'
470 commit = parse_commit(''.join(it))
471 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
473 def ordered_tree_entries(tree_data, bupm=None):
474 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
475 tree, sorted by name.
478 # Sadly, the .bupm entries currently aren't in git tree order,
479 # i.e. they don't account for the fact that git sorts trees
480 # (including our chunked trees) as if their names ended with "/",
481 # so "fo" sorts after "fo." iff fo is a directory. This makes
482 # streaming impossible when we need the metadata.
483 def result_from_tree_entry(tree_entry):
484 gitmode, mangled_name, oid = tree_entry
485 name, kind = git.demangle_name(mangled_name, gitmode)
486 return name, mangled_name, kind, gitmode, oid
488 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
490 tree_ents = sorted(tree_ents, key=lambda x: x[0])
491 for ent in tree_ents:
494 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
496 def tree_item(ent_oid, kind, gitmode):
497 if kind == BUP_CHUNKED:
498 meta = Metadata.read(bupm) if bupm else default_file_mode
499 return Chunky(oid=ent_oid, meta=meta)
502 # No metadata here (accessable via '.' inside ent_oid).
503 return Item(meta=default_dir_mode, oid=ent_oid)
505 return Item(oid=ent_oid,
506 meta=(Metadata.read(bupm) if bupm \
507 else _default_mode_for_gitmode(gitmode)))
509 assert len(oid) == 20
511 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
512 yield '.', Item(oid=oid, meta=dot_meta)
513 tree_entries = ordered_tree_entries(tree_data, bupm)
514 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
515 if mangled_name == '.bupm':
518 yield name, tree_item(ent_oid, kind, gitmode)
521 # Assumes the tree is properly formed, i.e. there are no
522 # duplicates, and entries will be in git tree order.
523 if type(names) not in (frozenset, set):
524 names = frozenset(names)
525 remaining = len(names)
527 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
528 last_name = max(names) if bupm else max(names) + '/'
531 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
532 yield '.', Item(oid=oid, meta=dot_meta)
537 tree_entries = ordered_tree_entries(tree_data, bupm)
538 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
539 if mangled_name == '.bupm':
542 if name not in names:
544 break # given bupm sort order, we're finished
545 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
548 yield name, tree_item(ent_oid, kind, gitmode)
553 def tree_items_with_meta(repo, oid, tree_data, names):
554 # For now, the .bupm order doesn't quite match git's, and we don't
555 # load the tree data incrementally anyway, so we just work in RAM
557 assert len(oid) == 20
559 for _, mangled_name, sub_oid in tree_decode(tree_data):
560 if mangled_name == '.bupm':
561 bupm = _FileReader(repo, sub_oid)
563 if mangled_name > '.bupm':
565 for item in tree_items(oid, tree_data, names, bupm):
568 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
570 def _reverse_suffix_duplicates(strs):
571 """Yields the elements of strs, with any runs of duplicate values
572 suffixed with -N suffixes, where the zero padded integer N
573 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
576 for name, duplicates in groupby(strs):
577 ndup = len(tuple(duplicates))
581 ndig = len(str(ndup - 1))
582 fmt = '%s-' + '%0' + str(ndig) + 'd'
583 for i in range(ndup - 1, -1, -1):
584 yield fmt % (name, i)
587 items = f.readline().split(None)
588 assert len(items) == 2
589 tree, auth_sec = items
590 return tree.decode('hex'), int(auth_sec)
592 def _name_for_rev(rev):
593 commit_oidx, (tree_oid, utc) = rev
594 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
596 def _item_for_rev(rev):
597 commit_oidx, (tree_oid, utc) = rev
598 coid = commit_oidx.decode('hex')
599 item = cache_get_commit_item(coid, need_meta=False)
602 item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
603 cache_notice(item.coid, item)
606 def cache_commit(repo, oid):
607 """Build, cache, and return a "name -> commit_item" dict of the entire
611 # For now, always cache with full metadata
613 entries['.'] = _revlist_item_from_oid(repo, oid, True)
614 revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
616 rev_items, rev_names = tee(revs)
617 revs = None # Don't disturb the tees
618 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
619 rev_items = (_item_for_rev(x) for x in rev_items)
621 for item in rev_items:
622 latest = latest or item
623 name = next(rev_names)
625 entries['latest'] = latest
626 cache_notice(latest.coid + b':r', entries)
629 def revlist_items(repo, oid, names):
630 assert len(oid) == 20
632 # Special case '.' instead of caching the whole history since it's
633 # the only way to get the metadata for the commit.
634 if names and all(x == '.' for x in names):
635 yield '.', _revlist_item_from_oid(repo, oid, True)
638 # For now, don't worry about the possibility of the contents being
639 # "too big" for the cache.
640 entries = cache_get(oid + b':r')
642 entries = cache_commit(repo, oid)
645 for name in sorted(entries.keys()):
646 yield name, entries[name]
649 names = frozenset(name for name in names
650 if _save_name_rx.match(name) or name in ('.', 'latest'))
653 yield '.', entries['.']
654 for name in (n for n in names if n != '.'):
655 commit = entries.get(name)
659 def tags_items(repo, names):
663 assert len(oid) == 20
664 oidx = oid.encode('hex')
666 _, typ, size = next(it)
668 return cache_get_commit_item(oid, need_meta=False) \
669 or _commit_item_from_data(oid, ''.join(it))
672 return Item(meta=default_file_mode, oid=oid)
674 return Item(meta=default_dir_mode, oid=oid)
675 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
679 # We have to pull these all into ram because tag_item calls cat()
680 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
681 assert(name.startswith('refs/tags/'))
683 yield name, tag_item(oid)
686 # Assumes no duplicate refs
687 if type(names) not in (frozenset, set):
688 names = frozenset(names)
689 remaining = len(names)
690 last_name = max(names)
697 for name, oid in repo.refs(names, limit_to_tags=True):
698 assert(name.startswith('refs/tags/'))
702 if name not in names:
704 yield name, tag_item(oid)
709 def contents(repo, item, names=None, want_meta=True):
710 """Yields information about the items contained in item. Yields
711 (name, item) for each name in names, if the name exists, in an
712 unspecified order. If there are no names, then yields (name,
713 item) for all items, including, a first item named '.'
714 representing the container itself.
716 The meta value for any directories other than '.' will be a
717 default directory mode, not a Metadata object. This is because
718 the actual metadata for a directory is stored inside the directory
719 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
721 Note that want_meta is advisory. For any given item, item.meta
722 might be a Metadata instance or a mode, and if the former,
723 meta.size might be None. Missing sizes can be computed via via
724 item_size() or augment_item_meta(..., include_size=True).
726 Do not modify any item.meta Metadata instances directly. If
727 needed, make a copy via item.meta.copy() and modify that instead.
730 # Q: are we comfortable promising '.' first when no names?
733 assert S_ISDIR(item_mode(item))
736 if item_t in real_tree_types:
737 it = repo.cat(item.oid.encode('hex'))
738 _, obj_type, size = next(it)
740 if obj_type == 'tree':
742 item_gen = tree_items_with_meta(repo, item.oid, data, names)
744 item_gen = tree_items(item.oid, data, names)
745 elif obj_type == 'commit':
747 item_gen = tree_items_with_meta(repo, item.oid, tree_data, names)
749 item_gen = tree_items(item.oid, tree_data, names)
752 raise Exception('unexpected git ' + obj_type)
753 elif item_t == RevList:
754 item_gen = revlist_items(repo, item.oid, names)
756 item_gen = root_items(repo, names, want_meta)
758 item_gen = tags_items(repo, names)
760 raise Exception('unexpected VFS item ' + str(item))
764 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
765 def raise_dir_required_but_not_dir(path, parent, past):
766 raise IOError(ENOTDIR,
767 "path %r%s resolves to non-directory %r"
769 ' (relative to %r)' % parent if parent else '',
778 assert type(x[0]) in (bytes, str)
779 assert type(x[1]) in item_types
780 assert parent[0][1] == _root
781 if not S_ISDIR(item_mode(parent[-1][1])):
782 raise IOError(ENOTDIR,
783 'path resolution parent %r is not a directory'
785 is_absolute, must_be_dir, future = _decompose_path(path)
788 if not future: # path was effectively '.' or '/'
790 return (('', _root),)
797 past = list(parent) if parent else [('', _root)]
801 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
802 raise_dir_required_but_not_dir(path, parent, past)
804 segment = future.pop()
807 if len(past) > 1: # .. from / is /
808 assert S_ISDIR(item_mode(past[-1][1]))
811 parent_name, parent_item = past[-1]
812 wanted = (segment,) if not want_meta else ('.', segment)
813 items = tuple(contents(repo, parent_item, names=wanted,
814 want_meta=want_meta))
816 item = items[0][1] if items else None
817 else: # First item will be '.' and have the metadata
818 item = items[1][1] if len(items) == 2 else None
819 dot, dot_item = items[0]
821 past[-1] = parent_name, parent_item
823 past.append((segment, None),)
825 mode = item_mode(item)
826 if not S_ISLNK(mode):
827 if not S_ISDIR(mode):
828 past.append((segment, item),)
830 raise IOError(ENOTDIR,
831 'path %r%s ends internally in non-directory here: %r'
833 ' (relative to %r)' % parent if parent else '',
837 raise_dir_required_but_not_dir(path, parent, past)
840 if want_meta and type(item) in real_tree_types:
841 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
843 item = item._replace(meta=dir_meta)
844 past.append((segment, item))
846 if not future and not deref:
847 past.append((segment, item),)
851 'too many symlinks encountered while resolving %r%s'
852 % (path, ' relative to %r' % parent if parent else ''),
853 terminus=tuple(past + [(segment, item)]))
854 target = readlink(repo, item)
855 is_absolute, _, target_future = _decompose_path(target)
857 if not target_future: # path was effectively '/'
858 return (('', _root),)
860 future = target_future
862 future.extend(target_future)
865 def lresolve(repo, path, parent=None, want_meta=True):
866 """Perform exactly the same function as resolve(), except if the final
867 path element is a symbolic link, don't follow it, just return it
871 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
874 def resolve(repo, path, parent=None, want_meta=True):
875 """Follow the path in the virtual filesystem and return a tuple
876 representing the location, if any, denoted by the path. Each
877 element in the result tuple will be (name, info), where info will
878 be a VFS item that can be passed to functions like item_mode().
880 If a path segment that does not exist is encountered during
881 resolution, the result will represent the location of the missing
882 item, and that item in the result will be None.
884 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
887 Any symlinks along the path, including at the end, will be
888 resolved. A VFS IOError with the errno attribute set to ELOOP
889 will be raised if too many symlinks are traversed while following
890 the path. That exception is effectively like a normal
891 ELOOP IOError exception, but will include a terminus element
892 describing the location of the failure, which will be a tuple of
893 (name, info) elements.
895 The parent, if specified, must be a sequence of (name, item)
896 tuples, and will provide the starting point for the resolution of
897 the path. If no parent is specified, resolution will start at
900 The result may include elements of parent directly, so they must
901 not be modified later. If this is a concern, pass in "name,
902 copy_item(item) for name, item in parent" instead.
904 When want_meta is true, detailed metadata will be included in each
905 result item if it's avaiable, otherwise item.meta will be an
906 integer mode. The metadata size may or may not be provided, but
907 can be computed by item_size() or augment_item_meta(...,
908 include_size=True). Setting want_meta=False is rarely desirable
909 since it can limit the VFS to just the metadata git itself can
910 represent, and so, as an example, fifos and sockets will appear to
911 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
912 But the option is provided because it may be more efficient when
913 only the path names or the more limited metadata is sufficient.
915 Do not modify any item.meta Metadata instances directly. If
916 needed, make a copy via item.meta.copy() and modify that instead.
919 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
921 _, leaf_item = result[-1]
923 assert not S_ISLNK(item_mode(leaf_item))
926 def try_resolve(repo, path, parent=None, want_meta=True):
927 """If path does not refer to a symlink, does not exist, or refers to a
928 valid symlink, behave exactly like resolve(). If path refers to
929 an invalid symlink, behave like lresolve.
932 res = lresolve(repo, path, parent=parent, want_meta=want_meta)
933 leaf_name, leaf_item = res[-1]
936 if not S_ISLNK(item_mode(leaf_item)):
938 deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
939 deref_name, deref_item = deref[-1]
944 def augment_item_meta(repo, item, include_size=False):
945 """Ensure item has a Metadata instance for item.meta. If item.meta is
946 currently a mode, replace it with a compatible "fake" Metadata
947 instance. If include_size is true, ensure item.meta.size is
948 correct, computing it if needed. If item.meta is a Metadata
949 instance, this call may modify it in place or replace it.
952 # If we actually had parallelism, we'd need locking...
955 if isinstance(m, Metadata):
956 if include_size and m.size is None:
957 m.size = _compute_item_size(repo, item)
958 return item._replace(meta=m)
963 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
965 target = _readlink(repo, item.oid)
966 meta.symlink_target = target
967 meta.size = len(target)
969 meta.size = _compute_item_size(repo, item)
970 return item._replace(meta=meta)
972 def fill_in_metadata_if_dir(repo, item):
973 """If item is a directory and item.meta is not a Metadata instance,
974 attempt to find the metadata for the directory. If found, return
975 a new item augmented to include that metadata. Otherwise, return
976 item. May be useful for the output of contents().
979 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
980 items = tuple(contents(repo, item, ('.',), want_meta=True))
981 assert len(items) == 1
982 assert items[0][0] == '.'
986 def ensure_item_has_metadata(repo, item, include_size=False):
987 """If item is a directory, attempt to find and add its metadata. If
988 the item still doesn't have a Metadata instance for item.meta,
989 give it one via augment_item_meta(). May be useful for the output
993 return augment_item_meta(repo,
994 fill_in_metadata_if_dir(repo, item),
995 include_size=include_size)