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
63 from bup.repo import LocalRepo, RemoteRepo
66 class IOError(exceptions.IOError):
67 def __init__(self, errno, message, terminus=None):
68 exceptions.IOError.__init__(self, errno, message)
69 self.terminus = terminus
71 default_file_mode = S_IFREG | 0o644
72 default_dir_mode = S_IFDIR | 0o755
73 default_symlink_mode = S_IFLNK | 0o755
75 def _default_mode_for_gitmode(gitmode):
77 return default_file_mode
79 return default_dir_mode
81 return default_symlink_mode
82 raise Exception('unexpected git mode ' + oct(gitmode))
84 def _normal_or_chunked_file_size(repo, oid):
85 """Return the size of the normal or chunked file indicated by oid."""
86 # FIXME: --batch-format CatPipe?
87 it = repo.cat(oid.encode('hex'))
88 _, obj_t, size = next(it)
90 while obj_t == 'tree':
91 mode, name, last_oid = last(tree_decode(''.join(it)))
93 it = repo.cat(last_oid.encode('hex'))
94 _, obj_t, size = next(it)
95 return ofs + sum(len(b) for b in it)
97 def _skip_chunks_before_offset(tree, offset):
98 prev_ent = next(tree, None)
103 ent_ofs = int(ent[1], 16)
105 return chain([prev_ent, ent], tree)
106 if ent_ofs == offset:
107 return chain([ent], tree)
111 def _tree_chunks(repo, tree, startofs):
112 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
113 assert(startofs >= 0)
114 # name is the chunk's hex offset in the original file
115 for mode, name, oid in _skip_chunks_before_offset(tree, startofs):
117 skipmore = startofs - ofs
120 it = repo.cat(oid.encode('hex'))
121 _, obj_t, size = next(it)
124 assert obj_t == 'tree'
125 for b in _tree_chunks(repo, tree_decode(data), skipmore):
128 assert obj_t == 'blob'
129 yield data[skipmore:]
132 def __init__(self, repo, oid, startofs):
133 it = repo.cat(oid.encode('hex'))
134 _, obj_t, size = next(it)
135 isdir = obj_t == 'tree'
138 self.it = _tree_chunks(repo, tree_decode(data), startofs)
142 self.blob = data[startofs:]
145 def next(self, size):
147 while len(out) < size:
148 if self.it and not self.blob:
150 self.blob = self.it.next()
151 except StopIteration:
154 want = size - len(out)
155 out += self.blob[:want]
156 self.blob = self.blob[want:]
159 debug2('next(%d) returned %d\n' % (size, len(out)))
163 class _FileReader(object):
164 def __init__(self, repo, oid, known_size=None):
165 assert len(oid) == 20
170 self._size = known_size
172 def _compute_size(self):
174 self._size = _normal_or_chunked_file_size(self._repo, self.oid)
178 if ofs < 0 or ofs > self._compute_size():
179 raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
185 def read(self, count=-1):
186 size = self._compute_size()
190 count = size - self.ofs
191 if not self.reader or self.reader.ofs != self.ofs:
192 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
194 buf = self.reader.next(count)
197 raise # our offsets will be all screwed up otherwise
206 def __exit__(self, type, value, traceback):
210 _multiple_slashes_rx = re.compile(r'//+')
212 def _decompose_path(path):
213 """Return a boolean indicating whether the path is absolute, and a
214 reversed list of path elements, omitting any occurrences of "."
215 and ignoring any leading or trailing slash. If the path is
216 effectively '/' or '.', return an empty list.
219 path = re.sub(_multiple_slashes_rx, '/', path)
221 return True, True, []
222 is_absolute = must_be_dir = False
223 if path.startswith('/'):
226 for suffix in ('/', '/.'):
227 if path.endswith(suffix):
229 path = path[:-len(suffix)]
230 parts = [x for x in path.split('/') if x != '.']
233 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
234 return is_absolute, must_be_dir, parts
237 Item = namedtuple('Item', ('meta', 'oid'))
238 Chunky = namedtuple('Chunky', ('meta', 'oid'))
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 commit_oid + ':r' -> rev-list
273 i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
275 # Suspect we may eventually add "(container_oid, name) -> ...", and others.
280 if len(x) == 22 and x.endswith(b':r'):
282 if x.startswith('res:'):
287 assert is_valid_cache_key(key)
288 return _cache.get(key)
290 def cache_notice(key, value):
291 global _cache, _cache_keys, _cache_max_items
292 assert is_valid_cache_key(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 item = cache_get(oid)
314 if isinstance(item.meta, Metadata):
316 entries = cache_get(oid + b':r')
320 def cache_get_revlist_item(oid, need_meta=True):
321 commit = cache_get_commit_item(oid, need_meta=need_meta)
323 return RevList(oid=oid, meta=commit.meta)
326 """Return a completely independent copy of item, such that
327 modifications will not affect the original.
330 meta = getattr(item, 'meta', None)
331 if isinstance(meta, Metadata):
332 return(item._replace(meta=meta.copy()))
336 """Return the integer mode (stat st_mode) for item."""
338 if isinstance(m, Metadata):
342 def _read_dir_meta(bupm):
343 # This is because save writes unmodified Metadata() entries for
344 # fake parents -- test-save-strip-graft.sh demonstrates.
345 m = Metadata.read(bupm)
347 return default_dir_mode
348 assert m.mode is not None
353 def tree_data_and_bupm(repo, oid):
354 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
355 tree has no metadata (i.e. older bup save, or non-bup tree).
358 assert len(oid) == 20
359 it = repo.cat(oid.encode('hex'))
360 _, item_t, size = next(it)
362 if item_t == 'commit':
363 commit = parse_commit(data)
364 it = repo.cat(commit.tree)
365 _, item_t, size = next(it)
367 assert item_t == 'tree'
368 elif item_t != 'tree':
369 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
370 for _, mangled_name, sub_oid in tree_decode(data):
371 if mangled_name == '.bupm':
373 if mangled_name > '.bupm':
377 def _find_treeish_oid_metadata(repo, oid):
378 """Return the metadata for the tree or commit oid, or None if the tree
379 has no metadata (i.e. older bup save, or non-bup tree).
382 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
384 with _FileReader(repo, bupm_oid) as meta_stream:
385 return _read_dir_meta(meta_stream)
388 def _readlink(repo, oid):
389 return ''.join(repo.join(oid.encode('hex')))
391 def readlink(repo, item):
392 """Return the link target of item, which must be a symlink. Reads the
393 target from the repository if necessary."""
395 assert S_ISLNK(item_mode(item))
396 if isinstance(item.meta, Metadata):
397 target = item.meta.symlink_target
400 return _readlink(repo, item.oid)
402 def _compute_item_size(repo, item):
403 mode = item_mode(item)
405 size = _normal_or_chunked_file_size(repo, item.oid)
408 return len(_readlink(repo, item.oid))
411 def item_size(repo, item):
412 """Return the size of item, computing it if necessary."""
414 if isinstance(m, Metadata) and m.size is not None:
416 return _compute_item_size(repo, item)
418 def tree_data_reader(repo, oid):
419 """Return an open reader for all of the data contained within oid. If
420 oid refers to a tree, recursively concatenate all of its contents."""
421 return _FileReader(repo, oid)
423 def fopen(repo, item):
424 """Return an open reader for the given file item."""
425 assert S_ISREG(item_mode(item))
426 return tree_data_reader(repo, item.oid)
428 def _commit_item_from_data(oid, data):
429 info = parse_commit(data)
430 return Commit(meta=default_dir_mode,
431 oid=info.tree.decode('hex'),
434 def _commit_item_from_oid(repo, oid, require_meta):
435 commit = cache_get_commit_item(oid, need_meta=require_meta)
436 if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
438 it = repo.cat(oid.encode('hex'))
439 _, typ, size = next(it)
440 assert typ == 'commit'
441 commit = _commit_item_from_data(oid, ''.join(it))
443 meta = _find_treeish_oid_metadata(repo, commit.oid)
445 commit = commit._replace(meta=meta)
446 cache_notice(oid, commit)
449 def _revlist_item_from_oid(repo, oid, require_meta):
450 commit = _commit_item_from_oid(repo, oid, require_meta)
451 return RevList(oid=oid, meta=commit.meta)
453 def root_items(repo, names=None, want_meta=True):
454 """Yield (name, item) for the items in '/' in the VFS. Return
455 everything if names is logically false, otherwise return only
456 items with a name in the collection.
459 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
465 # FIXME: maybe eventually support repo.clone() or something
466 # and pass in two repos, so we can drop the tuple() and stream
467 # in parallel (i.e. meta vs refs).
468 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
469 assert(name.startswith('refs/heads/'))
470 yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
478 if ref in ('.', '.tag'):
480 it = repo.cat('refs/heads/' + ref)
481 oidx, typ, size = next(it)
485 assert typ == 'commit'
486 commit = parse_commit(''.join(it))
487 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
489 def ordered_tree_entries(tree_data, bupm=None):
490 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
491 tree, sorted by name.
494 # Sadly, the .bupm entries currently aren't in git tree order,
495 # i.e. they don't account for the fact that git sorts trees
496 # (including our chunked trees) as if their names ended with "/",
497 # so "fo" sorts after "fo." iff fo is a directory. This makes
498 # streaming impossible when we need the metadata.
499 def result_from_tree_entry(tree_entry):
500 gitmode, mangled_name, oid = tree_entry
501 name, kind = git.demangle_name(mangled_name, gitmode)
502 return name, mangled_name, kind, gitmode, oid
504 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
506 tree_ents = sorted(tree_ents, key=lambda x: x[0])
507 for ent in tree_ents:
510 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
512 def tree_item(ent_oid, kind, gitmode):
513 if kind == BUP_CHUNKED:
514 meta = Metadata.read(bupm) if bupm else default_file_mode
515 return Chunky(oid=ent_oid, meta=meta)
518 # No metadata here (accessable via '.' inside ent_oid).
519 return Item(meta=default_dir_mode, oid=ent_oid)
521 return Item(oid=ent_oid,
522 meta=(Metadata.read(bupm) if bupm \
523 else _default_mode_for_gitmode(gitmode)))
525 assert len(oid) == 20
527 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
528 yield '.', Item(oid=oid, meta=dot_meta)
529 tree_entries = ordered_tree_entries(tree_data, bupm)
530 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
531 if mangled_name == '.bupm':
534 yield name, tree_item(ent_oid, kind, gitmode)
537 # Assumes the tree is properly formed, i.e. there are no
538 # duplicates, and entries will be in git tree order.
539 if type(names) not in (frozenset, set):
540 names = frozenset(names)
541 remaining = len(names)
543 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
544 last_name = max(names) if bupm else max(names) + '/'
547 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
548 yield '.', Item(oid=oid, meta=dot_meta)
553 tree_entries = ordered_tree_entries(tree_data, bupm)
554 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
555 if mangled_name == '.bupm':
558 if name not in names:
560 break # given bupm sort order, we're finished
561 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
564 yield name, tree_item(ent_oid, kind, gitmode)
569 def tree_items_with_meta(repo, oid, tree_data, names):
570 # For now, the .bupm order doesn't quite match git's, and we don't
571 # load the tree data incrementally anyway, so we just work in RAM
573 assert len(oid) == 20
575 for _, mangled_name, sub_oid in tree_decode(tree_data):
576 if mangled_name == '.bupm':
577 bupm = _FileReader(repo, sub_oid)
579 if mangled_name > '.bupm':
581 for item in tree_items(oid, tree_data, names, bupm):
584 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
586 def _reverse_suffix_duplicates(strs):
587 """Yields the elements of strs, with any runs of duplicate values
588 suffixed with -N suffixes, where the zero padded integer N
589 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
592 for name, duplicates in groupby(strs):
593 ndup = len(tuple(duplicates))
597 ndig = len(str(ndup - 1))
598 fmt = '%s-' + '%0' + str(ndig) + 'd'
599 for i in range(ndup - 1, -1, -1):
600 yield fmt % (name, i)
603 items = f.readline().split(None)
604 assert len(items) == 2
605 tree, auth_sec = items
606 return tree.decode('hex'), int(auth_sec)
608 def _name_for_rev(rev):
609 commit_oidx, (tree_oid, utc) = rev
610 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
612 def _item_for_rev(rev):
613 commit_oidx, (tree_oid, utc) = rev
614 coid = commit_oidx.decode('hex')
615 item = cache_get_commit_item(coid, need_meta=False)
618 item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
619 cache_notice(item.coid, item)
622 def cache_commit(repo, oid):
623 """Build, cache, and return a "name -> commit_item" dict of the entire
627 # For now, always cache with full metadata
629 entries['.'] = _revlist_item_from_oid(repo, oid, True)
630 revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
632 rev_items, rev_names = tee(revs)
633 revs = None # Don't disturb the tees
634 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
635 rev_items = (_item_for_rev(x) for x in rev_items)
637 for item in rev_items:
638 latest = latest or item
639 name = next(rev_names)
641 entries['latest'] = latest
642 cache_notice(latest.coid + b':r', entries)
645 def revlist_items(repo, oid, names):
646 assert len(oid) == 20
648 # Special case '.' instead of caching the whole history since it's
649 # the only way to get the metadata for the commit.
650 if names and all(x == '.' for x in names):
651 yield '.', _revlist_item_from_oid(repo, oid, True)
654 # For now, don't worry about the possibility of the contents being
655 # "too big" for the cache.
656 entries = cache_get(oid + b':r')
658 entries = cache_commit(repo, oid)
661 for name in sorted(entries.keys()):
662 yield name, entries[name]
665 names = frozenset(name for name in names
666 if _save_name_rx.match(name) or name in ('.', 'latest'))
669 yield '.', entries['.']
670 for name in (n for n in names if n != '.'):
671 commit = entries.get(name)
675 def tags_items(repo, names):
679 assert len(oid) == 20
680 oidx = oid.encode('hex')
682 _, typ, size = next(it)
684 return cache_get_commit_item(oid, need_meta=False) \
685 or _commit_item_from_data(oid, ''.join(it))
688 return Item(meta=default_file_mode, oid=oid)
690 return Item(meta=default_dir_mode, oid=oid)
691 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
695 # We have to pull these all into ram because tag_item calls cat()
696 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
697 assert(name.startswith('refs/tags/'))
699 yield name, tag_item(oid)
702 # Assumes no duplicate refs
703 if type(names) not in (frozenset, set):
704 names = frozenset(names)
705 remaining = len(names)
706 last_name = max(names)
713 for name, oid in repo.refs(names, limit_to_tags=True):
714 assert(name.startswith('refs/tags/'))
718 if name not in names:
720 yield name, tag_item(oid)
725 def contents(repo, item, names=None, want_meta=True):
726 """Yields information about the items contained in item. Yields
727 (name, item) for each name in names, if the name exists, in an
728 unspecified order. If there are no names, then yields (name,
729 item) for all items, including, a first item named '.'
730 representing the container itself.
732 The meta value for any directories other than '.' will be a
733 default directory mode, not a Metadata object. This is because
734 the actual metadata for a directory is stored inside the directory
735 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
737 Note that want_meta is advisory. For any given item, item.meta
738 might be a Metadata instance or a mode, and if the former,
739 meta.size might be None. Missing sizes can be computed via via
740 item_size() or augment_item_meta(..., include_size=True).
742 Do not modify any item.meta Metadata instances directly. If
743 needed, make a copy via item.meta.copy() and modify that instead.
746 # Q: are we comfortable promising '.' first when no names?
749 assert S_ISDIR(item_mode(item))
751 if item_t in real_tree_types:
752 it = repo.cat(item.oid.encode('hex'))
753 _, obj_t, size = next(it)
757 # Note: it shouldn't be possible to see an Item with type
758 # 'commit' since a 'commit' should always produce a Commit.
759 raise Exception('unexpected git ' + obj_t)
761 item_gen = tree_items_with_meta(repo, item.oid, data, names)
763 item_gen = tree_items(item.oid, data, names)
764 elif item_t == RevList:
765 item_gen = revlist_items(repo, item.oid, names)
767 item_gen = root_items(repo, names, want_meta)
769 item_gen = tags_items(repo, names)
771 raise Exception('unexpected VFS item ' + str(item))
775 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
776 cache_key = b'res:%d%d%d:%s\0%s' \
777 % (bool(want_meta), bool(deref), repo.id(),
778 ('/'.join(x[0] for x in parent) if parent else ''),
780 resolution = cache_get(cache_key)
784 def notice_resolution(r):
785 cache_notice(cache_key, r)
788 def raise_dir_required_but_not_dir(path, parent, past):
789 raise IOError(ENOTDIR,
790 "path %r%s resolves to non-directory %r"
792 ' (relative to %r)' % parent if parent else '',
801 assert type(x[0]) in (bytes, str)
802 assert type(x[1]) in item_types
803 assert parent[0][1] == _root
804 if not S_ISDIR(item_mode(parent[-1][1])):
805 raise IOError(ENOTDIR,
806 'path resolution parent %r is not a directory'
808 is_absolute, must_be_dir, future = _decompose_path(path)
811 if not future: # path was effectively '.' or '/'
813 return notice_resolution((('', _root),))
815 return notice_resolution(tuple(parent))
816 return notice_resolution((('', _root),))
820 past = list(parent) if parent else [('', _root)]
824 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
825 raise_dir_required_but_not_dir(path, parent, past)
826 return notice_resolution(tuple(past))
827 segment = future.pop()
830 if len(past) > 1: # .. from / is /
831 assert S_ISDIR(item_mode(past[-1][1]))
834 parent_name, parent_item = past[-1]
835 wanted = (segment,) if not want_meta else ('.', segment)
836 items = tuple(contents(repo, parent_item, names=wanted,
837 want_meta=want_meta))
839 item = items[0][1] if items else None
840 else: # First item will be '.' and have the metadata
841 item = items[1][1] if len(items) == 2 else None
842 dot, dot_item = items[0]
844 past[-1] = parent_name, parent_item
846 past.append((segment, None),)
847 return notice_resolution(tuple(past))
848 mode = item_mode(item)
849 if not S_ISLNK(mode):
850 if not S_ISDIR(mode):
851 past.append((segment, item),)
853 raise IOError(ENOTDIR,
854 'path %r%s ends internally in non-directory here: %r'
856 ' (relative to %r)' % parent if parent else '',
860 raise_dir_required_but_not_dir(path, parent, past)
861 return notice_resolution(tuple(past))
863 if want_meta and type(item) in real_tree_types:
864 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
866 item = item._replace(meta=dir_meta)
867 past.append((segment, item))
869 if not future and not deref:
870 past.append((segment, item),)
874 'too many symlinks encountered while resolving %r%s'
875 % (path, ' relative to %r' % parent if parent else ''),
876 terminus=tuple(past + [(segment, item)]))
877 target = readlink(repo, item)
878 is_absolute, _, target_future = _decompose_path(target)
880 if not target_future: # path was effectively '/'
881 return notice_resolution((('', _root),))
883 future = target_future
885 future.extend(target_future)
888 def lresolve(repo, path, parent=None, want_meta=True):
889 """Perform exactly the same function as resolve(), except if the final
890 path element is a symbolic link, don't follow it, just return it
894 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
897 def resolve(repo, path, parent=None, want_meta=True):
898 """Follow the path in the virtual filesystem and return a tuple
899 representing the location, if any, denoted by the path. Each
900 element in the result tuple will be (name, info), where info will
901 be a VFS item that can be passed to functions like item_mode().
903 If a path segment that does not exist is encountered during
904 resolution, the result will represent the location of the missing
905 item, and that item in the result will be None.
907 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
910 Any symlinks along the path, including at the end, will be
911 resolved. A VFS IOError with the errno attribute set to ELOOP
912 will be raised if too many symlinks are traversed while following
913 the path. That exception is effectively like a normal
914 ELOOP IOError exception, but will include a terminus element
915 describing the location of the failure, which will be a tuple of
916 (name, info) elements.
918 The parent, if specified, must be a sequence of (name, item)
919 tuples, and will provide the starting point for the resolution of
920 the path. If no parent is specified, resolution will start at
923 The result may include elements of parent directly, so they must
924 not be modified later. If this is a concern, pass in "name,
925 copy_item(item) for name, item in parent" instead.
927 When want_meta is true, detailed metadata will be included in each
928 result item if it's avaiable, otherwise item.meta will be an
929 integer mode. The metadata size may or may not be provided, but
930 can be computed by item_size() or augment_item_meta(...,
931 include_size=True). Setting want_meta=False is rarely desirable
932 since it can limit the VFS to just the metadata git itself can
933 represent, and so, as an example, fifos and sockets will appear to
934 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
935 But the option is provided because it may be more efficient when
936 only the path names or the more limited metadata is sufficient.
938 Do not modify any item.meta Metadata instances directly. If
939 needed, make a copy via item.meta.copy() and modify that instead.
942 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
944 _, leaf_item = result[-1]
946 assert not S_ISLNK(item_mode(leaf_item))
949 def try_resolve(repo, path, parent=None, want_meta=True):
950 """If path does not refer to a symlink, does not exist, or refers to a
951 valid symlink, behave exactly like resolve(). If path refers to
952 an invalid symlink, behave like lresolve.
955 res = lresolve(repo, path, parent=parent, want_meta=want_meta)
956 leaf_name, leaf_item = res[-1]
959 if not S_ISLNK(item_mode(leaf_item)):
961 deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
962 deref_name, deref_item = deref[-1]
967 def augment_item_meta(repo, item, include_size=False):
968 """Ensure item has a Metadata instance for item.meta. If item.meta is
969 currently a mode, replace it with a compatible "fake" Metadata
970 instance. If include_size is true, ensure item.meta.size is
971 correct, computing it if needed. If item.meta is a Metadata
972 instance, this call may modify it in place or replace it.
975 # If we actually had parallelism, we'd need locking...
978 if isinstance(m, Metadata):
979 if include_size and m.size is None:
980 m.size = _compute_item_size(repo, item)
981 return item._replace(meta=m)
986 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
988 target = _readlink(repo, item.oid)
989 meta.symlink_target = target
990 meta.size = len(target)
992 meta.size = _compute_item_size(repo, item)
993 return item._replace(meta=meta)
995 def fill_in_metadata_if_dir(repo, item):
996 """If item is a directory and item.meta is not a Metadata instance,
997 attempt to find the metadata for the directory. If found, return
998 a new item augmented to include that metadata. Otherwise, return
999 item. May be useful for the output of contents().
1002 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
1003 items = tuple(contents(repo, item, ('.',), want_meta=True))
1004 assert len(items) == 1
1005 assert items[0][0] == '.'
1009 def ensure_item_has_metadata(repo, item, include_size=False):
1010 """If item is a directory, attempt to find and add its metadata. If
1011 the item still doesn't have a Metadata instance for item.meta,
1012 give it one via augment_item_meta(). May be useful for the output
1016 return augment_item_meta(repo,
1017 fill_in_metadata_if_dir(repo, item),
1018 include_size=include_size)