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, or call
19 The want_meta argument is advisory for calls that accept it, and it
20 may not be honored. Callers must be able to handle an item.meta value
21 that is either an instance of Metadata or an integer mode, perhaps
22 via item_mode() or augment_item_meta().
24 Setting want_meta=False is rarely desirable since it can limit the VFS
25 to only the metadata that git itself can represent, and so for
26 example, fifos and sockets will appear to be regular files
27 (e.g. S_ISREG(item_mode(item)) will be true). But the option is still
28 provided because it may be more efficient when just the path names or
29 the more limited metadata is sufficient.
31 Any given metadata object's size may be None, in which case the size
32 can be computed via item_size() or augment_item_meta(...,
35 When traversing a directory using functions like contents(), the meta
36 value for any directories other than '.' will be a default directory
37 mode, not a Metadata object. This is because the actual metadata for
38 a directory is stored inside the directory (see
39 fill_in_metadata_if_dir() or ensure_item_has_metadata()).
41 Commit items represent commits (e.g. /.tag/some-commit or
42 /foo/latest), and for most purposes, they appear as the underlying
43 tree. S_ISDIR(item_mode(item)) will return true for both tree Items
44 and Commits and the commit's oid is the tree hash; the commit hash is
49 from __future__ import absolute_import, print_function
50 from collections import namedtuple
51 from errno import EINVAL, ELOOP, ENOENT, ENOTDIR
52 from itertools import chain, dropwhile, groupby, tee
53 from random import randrange
54 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
55 from time import localtime, strftime
56 import exceptions, re, sys
58 from bup import client, git, metadata
59 from bup.compat import range
60 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
61 from bup.helpers import debug2, last
62 from bup.metadata import Metadata
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 _skip_chunks_before_offset(tree, offset):
97 prev_ent = next(tree, None)
102 ent_ofs = int(ent[1], 16)
104 return chain([prev_ent, ent], tree)
105 if ent_ofs == offset:
106 return chain([ent], tree)
110 def _tree_chunks(repo, tree, startofs):
111 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
112 assert(startofs >= 0)
113 # name is the chunk's hex offset in the original file
114 for mode, name, oid in _skip_chunks_before_offset(tree, startofs):
116 skipmore = startofs - ofs
119 it = repo.cat(oid.encode('hex'))
120 _, obj_t, size = next(it)
123 assert obj_t == 'tree'
124 for b in _tree_chunks(repo, tree_decode(data), skipmore):
127 assert obj_t == 'blob'
128 yield data[skipmore:]
131 def __init__(self, repo, oid, startofs):
132 it = repo.cat(oid.encode('hex'))
133 _, obj_t, size = next(it)
134 isdir = obj_t == 'tree'
137 self.it = _tree_chunks(repo, tree_decode(data), startofs)
141 self.blob = data[startofs:]
144 def next(self, size):
146 while len(out) < size:
147 if self.it and not self.blob:
149 self.blob = self.it.next()
150 except StopIteration:
153 want = size - len(out)
154 out += self.blob[:want]
155 self.blob = self.blob[want:]
158 debug2('next(%d) returned %d\n' % (size, len(out)))
162 class _FileReader(object):
163 def __init__(self, repo, oid, known_size=None):
164 assert len(oid) == 20
169 self._size = known_size
171 def _compute_size(self):
173 self._size = _normal_or_chunked_file_size(self._repo, self.oid)
177 if ofs < 0 or ofs > self._compute_size():
178 raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
184 def read(self, count=-1):
185 size = self._compute_size()
189 count = size - self.ofs
190 if not self.reader or self.reader.ofs != self.ofs:
191 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
193 buf = self.reader.next(count)
196 raise # our offsets will be all screwed up otherwise
205 def __exit__(self, type, value, traceback):
209 _multiple_slashes_rx = re.compile(r'//+')
211 def _decompose_path(path):
212 """Return a boolean indicating whether the path is absolute, and a
213 reversed list of path elements, omitting any occurrences of "."
214 and ignoring any leading or trailing slash. If the path is
215 effectively '/' or '.', return an empty list.
218 path = re.sub(_multiple_slashes_rx, '/', path)
220 return True, True, []
221 is_absolute = must_be_dir = False
222 if path.startswith('/'):
225 for suffix in ('/', '/.'):
226 if path.endswith(suffix):
228 path = path[:-len(suffix)]
229 parts = [x for x in path.split('/') if x != '.']
232 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
233 return is_absolute, must_be_dir, parts
236 Item = namedtuple('Item', ('meta', 'oid'))
237 Chunky = namedtuple('Chunky', ('meta', 'oid'))
238 FakeLink = namedtuple('FakeLink', ('meta', 'target'))
239 Root = namedtuple('Root', ('meta'))
240 Tags = namedtuple('Tags', ('meta'))
241 RevList = namedtuple('RevList', ('meta', 'oid'))
242 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
244 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
245 real_tree_types = frozenset((Item, Commit))
247 _root = Root(meta=default_dir_mode)
248 _tags = Tags(meta=default_dir_mode)
253 ### A general purpose shared cache with (currently) cheap random
254 ### eviction. At the moment there is no weighting so a single commit
255 ### item is just as likely to be evicted as an entire "rev-list". See
256 ### is_valid_cache_key for a description of the expected content.
260 _cache_max_items = 30000
263 global _cache, _cache_keys
267 def is_valid_cache_key(x):
268 """Return logically true if x looks like it could be a valid cache key
269 (with respect to structure). Current valid cache entries:
270 res:... -> resolution
272 rvl:OID -> {'.', commit, '2012...', next_commit, ...}
274 # Suspect we may eventually add "(container_oid, name) -> ...", and others.
278 if tag in ('itm:', 'rvl:') and len(x) == 24:
285 if not is_valid_cache_key(key):
286 raise Exception('invalid cache key: ' + repr(key))
287 return _cache.get(key)
289 def cache_notice(key, value):
290 global _cache, _cache_keys, _cache_max_items
291 if not is_valid_cache_key(key):
292 raise Exception('invalid cache key: ' + repr(key))
295 if len(_cache) < _cache_max_items:
296 _cache_keys.append(key)
299 victim_i = randrange(0, len(_cache_keys))
300 victim = _cache_keys[victim_i]
302 _cache_keys[victim_i] = key
305 def cache_get_commit_item(oid, need_meta=True):
306 """Return the requested tree item if it can be found in the cache.
307 When need_meta is true don't return a cached item that only has a
309 # tree might be stored independently, or as '.' with its entries.
310 commit_key = b'itm:' + oid
311 item = cache_get(commit_key)
315 if isinstance(item.meta, Metadata):
317 entries = cache_get(b'rvl:' + oid)
321 def cache_get_revlist_item(oid, need_meta=True):
322 commit = cache_get_commit_item(oid, need_meta=need_meta)
324 return RevList(oid=oid, meta=commit.meta)
327 """Return a completely independent copy of item, such that
328 modifications will not affect the original.
331 meta = getattr(item, 'meta', None)
332 if isinstance(meta, Metadata):
333 return(item._replace(meta=meta.copy()))
337 """Return the integer mode (stat st_mode) for item."""
339 if isinstance(m, Metadata):
343 def _read_dir_meta(bupm):
344 # This is because save writes unmodified Metadata() entries for
345 # fake parents -- test-save-strip-graft.sh demonstrates.
346 m = Metadata.read(bupm)
348 return default_dir_mode
349 assert m.mode is not None
354 def tree_data_and_bupm(repo, oid):
355 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
356 tree has no metadata (i.e. older bup save, or non-bup tree).
359 assert len(oid) == 20
360 it = repo.cat(oid.encode('hex'))
361 _, item_t, size = next(it)
363 if item_t == 'commit':
364 commit = parse_commit(data)
365 it = repo.cat(commit.tree)
366 _, item_t, size = next(it)
368 assert item_t == 'tree'
369 elif item_t != 'tree':
370 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
371 for _, mangled_name, sub_oid in tree_decode(data):
372 if mangled_name == '.bupm':
374 if mangled_name > '.bupm':
378 def _find_treeish_oid_metadata(repo, oid):
379 """Return the metadata for the tree or commit oid, or None if the tree
380 has no metadata (i.e. older bup save, or non-bup tree).
383 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
385 with _FileReader(repo, bupm_oid) as meta_stream:
386 return _read_dir_meta(meta_stream)
389 def _readlink(repo, oid):
390 return ''.join(repo.join(oid.encode('hex')))
392 def readlink(repo, item):
393 """Return the link target of item, which must be a symlink. Reads the
394 target from the repository if necessary."""
396 assert S_ISLNK(item_mode(item))
397 if isinstance(item.meta, Metadata):
398 target = item.meta.symlink_target
401 elif isinstance(item, FakeLink):
403 return _readlink(repo, item.oid)
405 def _compute_item_size(repo, item):
406 mode = item_mode(item)
408 size = _normal_or_chunked_file_size(repo, item.oid)
411 return len(_readlink(repo, item.oid))
414 def item_size(repo, item):
415 """Return the size of item, computing it if necessary."""
417 if isinstance(m, Metadata) and m.size is not None:
419 return _compute_item_size(repo, item)
421 def tree_data_reader(repo, oid):
422 """Return an open reader for all of the data contained within oid. If
423 oid refers to a tree, recursively concatenate all of its contents."""
424 return _FileReader(repo, oid)
426 def fopen(repo, item):
427 """Return an open reader for the given file item."""
428 assert S_ISREG(item_mode(item))
429 return tree_data_reader(repo, item.oid)
431 def _commit_item_from_data(oid, data):
432 info = parse_commit(data)
433 return Commit(meta=default_dir_mode,
434 oid=info.tree.decode('hex'),
437 def _commit_item_from_oid(repo, oid, require_meta):
438 commit = cache_get_commit_item(oid, need_meta=require_meta)
439 if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
441 it = repo.cat(oid.encode('hex'))
442 _, typ, size = next(it)
443 assert typ == 'commit'
444 commit = _commit_item_from_data(oid, ''.join(it))
446 meta = _find_treeish_oid_metadata(repo, commit.oid)
448 commit = commit._replace(meta=meta)
449 commit_key = b'itm:' + oid
450 cache_notice(commit_key, commit)
453 def _revlist_item_from_oid(repo, oid, require_meta):
454 commit = _commit_item_from_oid(repo, oid, require_meta)
455 return RevList(oid=oid, meta=commit.meta)
457 def root_items(repo, names=None, want_meta=True):
458 """Yield (name, item) for the items in '/' in the VFS. Return
459 everything if names is logically false, otherwise return only
460 items with a name in the collection.
463 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
469 # FIXME: maybe eventually support repo.clone() or something
470 # and pass in two repos, so we can drop the tuple() and stream
471 # in parallel (i.e. meta vs refs).
472 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
473 assert(name.startswith('refs/heads/'))
474 yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
482 if ref in ('.', '.tag'):
484 it = repo.cat('refs/heads/' + ref)
485 oidx, typ, size = next(it)
489 assert typ == 'commit'
490 commit = parse_commit(''.join(it))
491 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
493 def ordered_tree_entries(tree_data, bupm=None):
494 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
495 tree, sorted by name.
498 # Sadly, the .bupm entries currently aren't in git tree order,
499 # i.e. they don't account for the fact that git sorts trees
500 # (including our chunked trees) as if their names ended with "/",
501 # so "fo" sorts after "fo." iff fo is a directory. This makes
502 # streaming impossible when we need the metadata.
503 def result_from_tree_entry(tree_entry):
504 gitmode, mangled_name, oid = tree_entry
505 name, kind = git.demangle_name(mangled_name, gitmode)
506 return name, mangled_name, kind, gitmode, oid
508 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
510 tree_ents = sorted(tree_ents, key=lambda x: x[0])
511 for ent in tree_ents:
514 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
516 def tree_item(ent_oid, kind, gitmode):
517 if kind == BUP_CHUNKED:
518 meta = Metadata.read(bupm) if bupm else default_file_mode
519 return Chunky(oid=ent_oid, meta=meta)
522 # No metadata here (accessable via '.' inside ent_oid).
523 return Item(meta=default_dir_mode, oid=ent_oid)
525 return Item(oid=ent_oid,
526 meta=(Metadata.read(bupm) if bupm \
527 else _default_mode_for_gitmode(gitmode)))
529 assert len(oid) == 20
531 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
532 yield '.', Item(oid=oid, meta=dot_meta)
533 tree_entries = ordered_tree_entries(tree_data, bupm)
534 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
535 if mangled_name == '.bupm':
538 yield name, tree_item(ent_oid, kind, gitmode)
541 # Assumes the tree is properly formed, i.e. there are no
542 # duplicates, and entries will be in git tree order.
543 if type(names) not in (frozenset, set):
544 names = frozenset(names)
545 remaining = len(names)
547 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
548 last_name = max(names) if bupm else max(names) + '/'
551 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
552 yield '.', Item(oid=oid, meta=dot_meta)
557 tree_entries = ordered_tree_entries(tree_data, bupm)
558 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
559 if mangled_name == '.bupm':
562 if name not in names:
564 break # given bupm sort order, we're finished
565 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
568 yield name, tree_item(ent_oid, kind, gitmode)
573 def tree_items_with_meta(repo, oid, tree_data, names):
574 # For now, the .bupm order doesn't quite match git's, and we don't
575 # load the tree data incrementally anyway, so we just work in RAM
577 assert len(oid) == 20
579 for _, mangled_name, sub_oid in tree_decode(tree_data):
580 if mangled_name == '.bupm':
581 bupm = _FileReader(repo, sub_oid)
583 if mangled_name > '.bupm':
585 for item in tree_items(oid, tree_data, names, bupm):
588 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
590 def _reverse_suffix_duplicates(strs):
591 """Yields the elements of strs, with any runs of duplicate values
592 suffixed with -N suffixes, where the zero padded integer N
593 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
596 for name, duplicates in groupby(strs):
597 ndup = len(tuple(duplicates))
601 ndig = len(str(ndup - 1))
602 fmt = '%s-' + '%0' + str(ndig) + 'd'
603 for i in range(ndup - 1, -1, -1):
604 yield fmt % (name, i)
607 items = f.readline().split(None)
608 assert len(items) == 2
609 tree, auth_sec = items
610 return tree.decode('hex'), int(auth_sec)
612 def _name_for_rev(rev):
613 commit_oidx, (tree_oid, utc) = rev
614 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
616 def _item_for_rev(rev):
617 commit_oidx, (tree_oid, utc) = rev
618 coid = commit_oidx.decode('hex')
619 item = cache_get_commit_item(coid, need_meta=False)
622 item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
623 commit_key = b'itm:' + coid
624 cache_notice(commit_key, item)
627 def cache_commit(repo, oid):
628 """Build, cache, and return a "name -> commit_item" dict of the entire
632 # For now, always cache with full metadata
634 entries['.'] = _revlist_item_from_oid(repo, oid, True)
635 revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
637 rev_items, rev_names = tee(revs)
638 revs = None # Don't disturb the tees
639 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
640 rev_items = (_item_for_rev(x) for x in rev_items)
642 for item in rev_items:
643 name = next(rev_names)
644 tip = tip or (name, item)
646 entries['latest'] = FakeLink(meta=default_symlink_mode, target=tip[0])
647 revlist_key = b'rvl:' + tip[1].coid
648 cache_notice(revlist_key, entries)
651 def revlist_items(repo, oid, names):
652 assert len(oid) == 20
654 # Special case '.' instead of caching the whole history since it's
655 # the only way to get the metadata for the commit.
656 if names and all(x == '.' for x in names):
657 yield '.', _revlist_item_from_oid(repo, oid, True)
660 # For now, don't worry about the possibility of the contents being
661 # "too big" for the cache.
662 revlist_key = b'rvl:' + oid
663 entries = cache_get(revlist_key)
665 entries = cache_commit(repo, oid)
668 for name in sorted(entries.keys()):
669 yield name, entries[name]
672 names = frozenset(name for name in names
673 if _save_name_rx.match(name) or name in ('.', 'latest'))
676 yield '.', entries['.']
677 for name in (n for n in names if n != '.'):
678 commit = entries.get(name)
682 def tags_items(repo, names):
686 assert len(oid) == 20
687 oidx = oid.encode('hex')
689 _, typ, size = next(it)
691 return cache_get_commit_item(oid, need_meta=False) \
692 or _commit_item_from_data(oid, ''.join(it))
695 return Item(meta=default_file_mode, oid=oid)
697 return Item(meta=default_dir_mode, oid=oid)
698 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
702 # We have to pull these all into ram because tag_item calls cat()
703 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
704 assert(name.startswith('refs/tags/'))
706 yield name, tag_item(oid)
709 # Assumes no duplicate refs
710 if type(names) not in (frozenset, set):
711 names = frozenset(names)
712 remaining = len(names)
713 last_name = max(names)
720 for name, oid in repo.refs(names, limit_to_tags=True):
721 assert(name.startswith('refs/tags/'))
725 if name not in names:
727 yield name, tag_item(oid)
732 def contents(repo, item, names=None, want_meta=True):
733 """Yields information about the items contained in item. Yields
734 (name, item) for each name in names, if the name exists, in an
735 unspecified order. If there are no names, then yields (name,
736 item) for all items, including, a first item named '.'
737 representing the container itself.
739 The meta value for any directories other than '.' will be a
740 default directory mode, not a Metadata object. This is because
741 the actual metadata for a directory is stored inside the directory
742 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
744 Note that want_meta is advisory. For any given item, item.meta
745 might be a Metadata instance or a mode, and if the former,
746 meta.size might be None. Missing sizes can be computed via via
747 item_size() or augment_item_meta(..., include_size=True).
749 Do not modify any item.meta Metadata instances directly. If
750 needed, make a copy via item.meta.copy() and modify that instead.
753 # Q: are we comfortable promising '.' first when no names?
756 assert S_ISDIR(item_mode(item))
758 if item_t in real_tree_types:
759 it = repo.cat(item.oid.encode('hex'))
760 _, obj_t, size = next(it)
764 # Note: it shouldn't be possible to see an Item with type
765 # 'commit' since a 'commit' should always produce a Commit.
766 raise Exception('unexpected git ' + obj_t)
768 item_gen = tree_items_with_meta(repo, item.oid, data, names)
770 item_gen = tree_items(item.oid, data, names)
771 elif item_t == RevList:
772 item_gen = revlist_items(repo, item.oid, names)
774 item_gen = root_items(repo, names, want_meta)
776 item_gen = tags_items(repo, names)
778 raise Exception('unexpected VFS item ' + str(item))
782 def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
783 cache_key = b'res:%d%d%d:%s\0%s' \
784 % (bool(want_meta), bool(follow), repo.id(),
785 ('/'.join(x[0] for x in parent) if parent else ''),
787 resolution = cache_get(cache_key)
791 def notice_resolution(r):
792 cache_notice(cache_key, r)
795 def raise_dir_required_but_not_dir(path, parent, past):
796 raise IOError(ENOTDIR,
797 "path %r%s resolves to non-directory %r"
799 ' (relative to %r)' % parent if parent else '',
808 assert type(x[0]) in (bytes, str)
809 assert type(x[1]) in item_types
810 assert parent[0][1] == _root
811 if not S_ISDIR(item_mode(parent[-1][1])):
812 raise IOError(ENOTDIR,
813 'path resolution parent %r is not a directory'
815 is_absolute, must_be_dir, future = _decompose_path(path)
818 if not future: # path was effectively '.' or '/'
820 return notice_resolution((('', _root),))
822 return notice_resolution(tuple(parent))
823 return notice_resolution((('', _root),))
827 past = list(parent) if parent else [('', _root)]
831 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
832 raise_dir_required_but_not_dir(path, parent, past)
833 return notice_resolution(tuple(past))
834 segment = future.pop()
837 if len(past) > 1: # .. from / is /
838 assert S_ISDIR(item_mode(past[-1][1]))
841 parent_name, parent_item = past[-1]
842 wanted = (segment,) if not want_meta else ('.', segment)
843 items = tuple(contents(repo, parent_item, names=wanted,
844 want_meta=want_meta))
846 item = items[0][1] if items else None
847 else: # First item will be '.' and have the metadata
848 item = items[1][1] if len(items) == 2 else None
849 dot, dot_item = items[0]
851 past[-1] = parent_name, parent_item
853 past.append((segment, None),)
854 return notice_resolution(tuple(past))
855 mode = item_mode(item)
856 if not S_ISLNK(mode):
857 if not S_ISDIR(mode):
858 past.append((segment, item),)
860 raise IOError(ENOTDIR,
861 'path %r%s ends internally in non-directory here: %r'
863 ' (relative to %r)' % parent if parent else '',
867 raise_dir_required_but_not_dir(path, parent, past)
868 return notice_resolution(tuple(past))
870 if want_meta and type(item) in real_tree_types:
871 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
873 item = item._replace(meta=dir_meta)
874 past.append((segment, item))
876 if not future and not follow:
877 past.append((segment, item),)
881 'too many symlinks encountered while resolving %r%s'
882 % (path, ' relative to %r' % parent if parent else ''),
883 terminus=tuple(past + [(segment, item)]))
884 target = readlink(repo, item)
885 is_absolute, _, target_future = _decompose_path(target)
887 if not target_future: # path was effectively '/'
888 return notice_resolution((('', _root),))
890 future = target_future
892 future.extend(target_future)
895 def resolve(repo, path, parent=None, want_meta=True, follow=True):
896 """Follow the path in the virtual filesystem and return a tuple
897 representing the location, if any, denoted by the path. Each
898 element in the result tuple will be (name, info), where info will
899 be a VFS item that can be passed to functions like item_mode().
901 If follow is false, and if the final path element is a symbolic
902 link, don't follow it, just return it in the result.
904 If a path segment that does not exist is encountered during
905 resolution, the result will represent the location of the missing
906 item, and that item in the result will be None.
908 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
911 Any symlinks along the path, including at the end, will be
912 resolved. A VFS IOError with the errno attribute set to ELOOP
913 will be raised if too many symlinks are traversed while following
914 the path. That exception is effectively like a normal
915 ELOOP IOError exception, but will include a terminus element
916 describing the location of the failure, which will be a tuple of
917 (name, info) elements.
919 The parent, if specified, must be a sequence of (name, item)
920 tuples, and will provide the starting point for the resolution of
921 the path. If no parent is specified, resolution will start at
924 The result may include elements of parent directly, so they must
925 not be modified later. If this is a concern, pass in "name,
926 copy_item(item) for name, item in parent" instead.
928 When want_meta is true, detailed metadata will be included in each
929 result item if it's avaiable, otherwise item.meta will be an
930 integer mode. The metadata size may or may not be provided, but
931 can be computed by item_size() or augment_item_meta(...,
932 include_size=True). Setting want_meta=False is rarely desirable
933 since it can limit the VFS to just the metadata git itself can
934 represent, and so, as an example, fifos and sockets will appear to
935 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
936 But the option is provided because it may be more efficient when
937 only the path names or the more limited metadata is sufficient.
939 Do not modify any item.meta Metadata instances directly. If
940 needed, make a copy via item.meta.copy() and modify that instead.
943 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
945 _, leaf_item = result[-1]
946 if leaf_item and follow:
947 assert not S_ISLNK(item_mode(leaf_item))
950 def try_resolve(repo, path, parent=None, want_meta=True):
951 """If path does not refer to a symlink, does not exist, or refers to a
952 valid symlink, behave exactly like resolve(..., follow=True). If
953 path refers to an invalid symlink, behave like resolve(...,
957 res = resolve(repo, path, parent=parent, want_meta=want_meta, follow=False)
958 leaf_name, leaf_item = res[-1]
961 if not S_ISLNK(item_mode(leaf_item)):
963 follow = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
964 follow_name, follow_item = follow[-1]
969 def augment_item_meta(repo, item, include_size=False):
970 """Ensure item has a Metadata instance for item.meta. If item.meta is
971 currently a mode, replace it with a compatible "fake" Metadata
972 instance. If include_size is true, ensure item.meta.size is
973 correct, computing it if needed. If item.meta is a Metadata
974 instance, this call may modify it in place or replace it.
977 # If we actually had parallelism, we'd need locking...
980 if isinstance(m, Metadata):
981 if include_size and m.size is None:
982 m.size = _compute_item_size(repo, item)
983 return item._replace(meta=m)
988 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
990 if isinstance(item, FakeLink):
993 target = _readlink(repo, item.oid)
994 meta.symlink_target = target
995 meta.size = len(target)
997 meta.size = _compute_item_size(repo, item)
998 return item._replace(meta=meta)
1000 def fill_in_metadata_if_dir(repo, item):
1001 """If item is a directory and item.meta is not a Metadata instance,
1002 attempt to find the metadata for the directory. If found, return
1003 a new item augmented to include that metadata. Otherwise, return
1004 item. May be useful for the output of contents().
1007 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
1008 items = tuple(contents(repo, item, ('.',), want_meta=True))
1009 assert len(items) == 1
1010 assert items[0][0] == '.'
1014 def ensure_item_has_metadata(repo, item, include_size=False):
1015 """If item is a directory, attempt to find and add its metadata. If
1016 the item still doesn't have a Metadata instance for item.meta,
1017 give it one via augment_item_meta(). May be useful for the output
1021 return augment_item_meta(repo,
1022 fill_in_metadata_if_dir(repo, item),
1023 include_size=include_size)