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 _tree_chunks(repo, tree, startofs):
98 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
100 # name is the chunk's hex offset in the original file
101 tree = dropwhile(lambda (_1, name, _2): int(name, 16) < startofs, tree)
102 for mode, name, oid in tree:
104 skipmore = startofs - ofs
107 it = repo.cat(oid.encode('hex'))
108 _, obj_t, size = next(it)
111 assert obj_t == 'tree'
112 for b in _tree_chunks(repo, tree_decode(data), skipmore):
115 assert obj_t == 'blob'
116 yield data[skipmore:]
119 def __init__(self, repo, oid, startofs):
120 it = repo.cat(oid.encode('hex'))
121 _, obj_t, size = next(it)
122 isdir = obj_t == 'tree'
125 self.it = _tree_chunks(repo, tree_decode(data), startofs)
129 self.blob = data[startofs:]
132 def next(self, size):
134 while len(out) < size:
135 if self.it and not self.blob:
137 self.blob = self.it.next()
138 except StopIteration:
141 want = size - len(out)
142 out += self.blob[:want]
143 self.blob = self.blob[want:]
146 debug2('next(%d) returned %d\n' % (size, len(out)))
150 class _FileReader(object):
151 def __init__(self, repo, oid, known_size=None):
152 assert len(oid) == 20
157 self._size = known_size
159 def _compute_size(self):
161 self._size = _normal_or_chunked_file_size(self._repo, self.oid)
165 if ofs < 0 or ofs > self._compute_size():
166 raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
172 def read(self, count=-1):
173 size = self._compute_size()
177 count = size - self.ofs
178 if not self.reader or self.reader.ofs != self.ofs:
179 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
181 buf = self.reader.next(count)
184 raise # our offsets will be all screwed up otherwise
193 def __exit__(self, type, value, traceback):
197 _multiple_slashes_rx = re.compile(r'//+')
199 def _decompose_path(path):
200 """Return a boolean indicating whether the path is absolute, and a
201 reversed list of path elements, omitting any occurrences of "."
202 and ignoring any leading or trailing slash. If the path is
203 effectively '/' or '.', return an empty list.
206 path = re.sub(_multiple_slashes_rx, '/', path)
208 return True, True, []
209 is_absolute = must_be_dir = False
210 if path.startswith('/'):
213 for suffix in ('/', '/.'):
214 if path.endswith(suffix):
216 path = path[:-len(suffix)]
217 parts = [x for x in path.split('/') if x != '.']
220 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
221 return is_absolute, must_be_dir, parts
224 Item = namedtuple('Item', ('meta', 'oid'))
225 Chunky = namedtuple('Chunky', ('meta', 'oid'))
226 Root = namedtuple('Root', ('meta'))
227 Tags = namedtuple('Tags', ('meta'))
228 RevList = namedtuple('RevList', ('meta', 'oid'))
229 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
231 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
232 real_tree_types = frozenset((Item, Commit))
234 _root = Root(meta=default_dir_mode)
235 _tags = Tags(meta=default_dir_mode)
240 ### A general purpose shared cache with (currently) cheap random
241 ### eviction. At the moment there is no weighting so a single commit
242 ### item is just as likely to be evicted as an entire "rev-list". See
243 ### is_valid_cache_key for a description of the expected content.
247 _cache_max_items = 30000
250 global _cache, _cache_keys
254 def is_valid_cache_key(x):
255 """Return logically true if x looks like it could be a valid cache key
256 (with respect to structure). Current valid cache entries:
257 res:... -> resolution
259 commit_oid + ':r' -> rev-list
260 i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
262 # Suspect we may eventually add "(container_oid, name) -> ...", and others.
267 if len(x) == 22 and x.endswith(b':r'):
269 if x.startswith('res:'):
274 assert is_valid_cache_key(key)
275 return _cache.get(key)
277 def cache_notice(key, value):
278 global _cache, _cache_keys, _cache_max_items
279 assert is_valid_cache_key(key)
282 if len(_cache) < _cache_max_items:
283 _cache_keys.append(key)
286 victim_i = randrange(0, len(_cache_keys))
287 victim = _cache_keys[victim_i]
289 _cache_keys[victim_i] = key
292 def cache_get_commit_item(oid, need_meta=True):
293 """Return the requested tree item if it can be found in the cache.
294 When need_meta is true don't return a cached item that only has a
296 # tree might be stored independently, or as '.' with its entries.
297 item = cache_get(oid)
301 if isinstance(item.meta, Metadata):
303 entries = cache_get(oid + b':r')
307 def cache_get_revlist_item(oid, need_meta=True):
308 commit = cache_get_commit_item(oid, need_meta=need_meta)
310 return RevList(oid=oid, meta=commit.meta)
313 """Return a completely independent copy of item, such that
314 modifications will not affect the original.
317 meta = getattr(item, 'meta', None)
318 if isinstance(meta, Metadata):
319 return(item._replace(meta=meta.copy()))
323 """Return the integer mode (stat st_mode) for item."""
325 if isinstance(m, Metadata):
329 def _read_dir_meta(bupm):
330 # This is because save writes unmodified Metadata() entries for
331 # fake parents -- test-save-strip-graft.sh demonstrates.
332 m = Metadata.read(bupm)
334 return default_dir_mode
335 assert m.mode is not None
340 def tree_data_and_bupm(repo, oid):
341 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
342 tree has no metadata (i.e. older bup save, or non-bup tree).
345 assert len(oid) == 20
346 it = repo.cat(oid.encode('hex'))
347 _, item_t, size = next(it)
349 if item_t == 'commit':
350 commit = parse_commit(data)
351 it = repo.cat(commit.tree)
352 _, item_t, size = next(it)
354 assert item_t == 'tree'
355 elif item_t != 'tree':
356 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
357 for _, mangled_name, sub_oid in tree_decode(data):
358 if mangled_name == '.bupm':
360 if mangled_name > '.bupm':
364 def _find_treeish_oid_metadata(repo, oid):
365 """Return the metadata for the tree or commit oid, or None if the tree
366 has no metadata (i.e. older bup save, or non-bup tree).
369 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
371 with _FileReader(repo, bupm_oid) as meta_stream:
372 return _read_dir_meta(meta_stream)
375 def _readlink(repo, oid):
376 return ''.join(repo.join(oid.encode('hex')))
378 def readlink(repo, item):
379 """Return the link target of item, which must be a symlink. Reads the
380 target from the repository if necessary."""
382 assert S_ISLNK(item_mode(item))
383 if isinstance(item.meta, Metadata):
384 target = item.meta.symlink_target
387 return _readlink(repo, item.oid)
389 def _compute_item_size(repo, item):
390 mode = item_mode(item)
392 size = _normal_or_chunked_file_size(repo, item.oid)
395 return len(_readlink(repo, item.oid))
398 def item_size(repo, item):
399 """Return the size of item, computing it if necessary."""
401 if isinstance(m, Metadata) and m.size is not None:
403 return _compute_item_size(repo, item)
405 def tree_data_reader(repo, oid):
406 """Return an open reader for all of the data contained within oid. If
407 oid refers to a tree, recursively concatenate all of its contents."""
408 return _FileReader(repo, oid)
410 def fopen(repo, item):
411 """Return an open reader for the given file item."""
412 assert S_ISREG(item_mode(item))
413 return tree_data_reader(repo, item.oid)
415 def _commit_item_from_data(oid, data):
416 info = parse_commit(data)
417 return Commit(meta=default_dir_mode,
418 oid=info.tree.decode('hex'),
421 def _commit_item_from_oid(repo, oid, require_meta):
422 commit = cache_get_commit_item(oid, need_meta=require_meta)
423 if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
425 it = repo.cat(oid.encode('hex'))
426 _, typ, size = next(it)
427 assert typ == 'commit'
428 commit = _commit_item_from_data(oid, ''.join(it))
430 meta = _find_treeish_oid_metadata(repo, commit.oid)
432 commit = commit._replace(meta=meta)
433 cache_notice(oid, commit)
436 def _revlist_item_from_oid(repo, oid, require_meta):
437 commit = _commit_item_from_oid(repo, oid, require_meta)
438 return RevList(oid=oid, meta=commit.meta)
440 def root_items(repo, names=None, want_meta=True):
441 """Yield (name, item) for the items in '/' in the VFS. Return
442 everything if names is logically false, otherwise return only
443 items with a name in the collection.
446 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
452 # FIXME: maybe eventually support repo.clone() or something
453 # and pass in two repos, so we can drop the tuple() and stream
454 # in parallel (i.e. meta vs refs).
455 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
456 assert(name.startswith('refs/heads/'))
457 yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
465 if ref in ('.', '.tag'):
467 it = repo.cat('refs/heads/' + ref)
468 oidx, typ, size = next(it)
472 assert typ == 'commit'
473 commit = parse_commit(''.join(it))
474 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
476 def ordered_tree_entries(tree_data, bupm=None):
477 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
478 tree, sorted by name.
481 # Sadly, the .bupm entries currently aren't in git tree order,
482 # i.e. they don't account for the fact that git sorts trees
483 # (including our chunked trees) as if their names ended with "/",
484 # so "fo" sorts after "fo." iff fo is a directory. This makes
485 # streaming impossible when we need the metadata.
486 def result_from_tree_entry(tree_entry):
487 gitmode, mangled_name, oid = tree_entry
488 name, kind = git.demangle_name(mangled_name, gitmode)
489 return name, mangled_name, kind, gitmode, oid
491 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
493 tree_ents = sorted(tree_ents, key=lambda x: x[0])
494 for ent in tree_ents:
497 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
499 def tree_item(ent_oid, kind, gitmode):
500 if kind == BUP_CHUNKED:
501 meta = Metadata.read(bupm) if bupm else default_file_mode
502 return Chunky(oid=ent_oid, meta=meta)
505 # No metadata here (accessable via '.' inside ent_oid).
506 return Item(meta=default_dir_mode, oid=ent_oid)
508 return Item(oid=ent_oid,
509 meta=(Metadata.read(bupm) if bupm \
510 else _default_mode_for_gitmode(gitmode)))
512 assert len(oid) == 20
514 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
515 yield '.', Item(oid=oid, meta=dot_meta)
516 tree_entries = ordered_tree_entries(tree_data, bupm)
517 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
518 if mangled_name == '.bupm':
521 yield name, tree_item(ent_oid, kind, gitmode)
524 # Assumes the tree is properly formed, i.e. there are no
525 # duplicates, and entries will be in git tree order.
526 if type(names) not in (frozenset, set):
527 names = frozenset(names)
528 remaining = len(names)
530 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
531 last_name = max(names) if bupm else max(names) + '/'
534 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
535 yield '.', Item(oid=oid, meta=dot_meta)
540 tree_entries = ordered_tree_entries(tree_data, bupm)
541 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
542 if mangled_name == '.bupm':
545 if name not in names:
547 break # given bupm sort order, we're finished
548 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
551 yield name, tree_item(ent_oid, kind, gitmode)
556 def tree_items_with_meta(repo, oid, tree_data, names):
557 # For now, the .bupm order doesn't quite match git's, and we don't
558 # load the tree data incrementally anyway, so we just work in RAM
560 assert len(oid) == 20
562 for _, mangled_name, sub_oid in tree_decode(tree_data):
563 if mangled_name == '.bupm':
564 bupm = _FileReader(repo, sub_oid)
566 if mangled_name > '.bupm':
568 for item in tree_items(oid, tree_data, names, bupm):
571 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
573 def _reverse_suffix_duplicates(strs):
574 """Yields the elements of strs, with any runs of duplicate values
575 suffixed with -N suffixes, where the zero padded integer N
576 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
579 for name, duplicates in groupby(strs):
580 ndup = len(tuple(duplicates))
584 ndig = len(str(ndup - 1))
585 fmt = '%s-' + '%0' + str(ndig) + 'd'
586 for i in range(ndup - 1, -1, -1):
587 yield fmt % (name, i)
590 items = f.readline().split(None)
591 assert len(items) == 2
592 tree, auth_sec = items
593 return tree.decode('hex'), int(auth_sec)
595 def _name_for_rev(rev):
596 commit_oidx, (tree_oid, utc) = rev
597 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
599 def _item_for_rev(rev):
600 commit_oidx, (tree_oid, utc) = rev
601 coid = commit_oidx.decode('hex')
602 item = cache_get_commit_item(coid, need_meta=False)
605 item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
606 cache_notice(item.coid, item)
609 def cache_commit(repo, oid):
610 """Build, cache, and return a "name -> commit_item" dict of the entire
614 # For now, always cache with full metadata
616 entries['.'] = _revlist_item_from_oid(repo, oid, True)
617 revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
619 rev_items, rev_names = tee(revs)
620 revs = None # Don't disturb the tees
621 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
622 rev_items = (_item_for_rev(x) for x in rev_items)
624 for item in rev_items:
625 latest = latest or item
626 name = next(rev_names)
628 entries['latest'] = latest
629 cache_notice(latest.coid + b':r', entries)
632 def revlist_items(repo, oid, names):
633 assert len(oid) == 20
635 # Special case '.' instead of caching the whole history since it's
636 # the only way to get the metadata for the commit.
637 if names and all(x == '.' for x in names):
638 yield '.', _revlist_item_from_oid(repo, oid, True)
641 # For now, don't worry about the possibility of the contents being
642 # "too big" for the cache.
643 entries = cache_get(oid + b':r')
645 entries = cache_commit(repo, oid)
648 for name in sorted(entries.keys()):
649 yield name, entries[name]
652 names = frozenset(name for name in names
653 if _save_name_rx.match(name) or name in ('.', 'latest'))
656 yield '.', entries['.']
657 for name in (n for n in names if n != '.'):
658 commit = entries.get(name)
662 def tags_items(repo, names):
666 assert len(oid) == 20
667 oidx = oid.encode('hex')
669 _, typ, size = next(it)
671 return cache_get_commit_item(oid, need_meta=False) \
672 or _commit_item_from_data(oid, ''.join(it))
675 return Item(meta=default_file_mode, oid=oid)
677 return Item(meta=default_dir_mode, oid=oid)
678 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
682 # We have to pull these all into ram because tag_item calls cat()
683 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
684 assert(name.startswith('refs/tags/'))
686 yield name, tag_item(oid)
689 # Assumes no duplicate refs
690 if type(names) not in (frozenset, set):
691 names = frozenset(names)
692 remaining = len(names)
693 last_name = max(names)
700 for name, oid in repo.refs(names, limit_to_tags=True):
701 assert(name.startswith('refs/tags/'))
705 if name not in names:
707 yield name, tag_item(oid)
712 def contents(repo, item, names=None, want_meta=True):
713 """Yields information about the items contained in item. Yields
714 (name, item) for each name in names, if the name exists, in an
715 unspecified order. If there are no names, then yields (name,
716 item) for all items, including, a first item named '.'
717 representing the container itself.
719 The meta value for any directories other than '.' will be a
720 default directory mode, not a Metadata object. This is because
721 the actual metadata for a directory is stored inside the directory
722 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
724 Note that want_meta is advisory. For any given item, item.meta
725 might be a Metadata instance or a mode, and if the former,
726 meta.size might be None. Missing sizes can be computed via via
727 item_size() or augment_item_meta(..., include_size=True).
729 Do not modify any item.meta Metadata instances directly. If
730 needed, make a copy via item.meta.copy() and modify that instead.
733 # Q: are we comfortable promising '.' first when no names?
736 assert S_ISDIR(item_mode(item))
738 if item_t in real_tree_types:
739 it = repo.cat(item.oid.encode('hex'))
740 _, obj_t, size = next(it)
744 # Note: it shouldn't be possible to see an Item with type
745 # 'commit' since a 'commit' should always produce a Commit.
746 raise Exception('unexpected git ' + obj_t)
748 item_gen = tree_items_with_meta(repo, item.oid, data, names)
750 item_gen = tree_items(item.oid, data, names)
751 elif item_t == RevList:
752 item_gen = revlist_items(repo, item.oid, names)
754 item_gen = root_items(repo, names, want_meta)
756 item_gen = tags_items(repo, names)
758 raise Exception('unexpected VFS item ' + str(item))
762 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
763 cache_key = b'res:%d%d%d:%s\0%s' \
764 % (bool(want_meta), bool(deref), repo.id(),
765 ('/'.join(x[0] for x in parent) if parent else ''),
767 resolution = cache_get(cache_key)
771 def notice_resolution(r):
772 cache_notice(cache_key, r)
775 def raise_dir_required_but_not_dir(path, parent, past):
776 raise IOError(ENOTDIR,
777 "path %r%s resolves to non-directory %r"
779 ' (relative to %r)' % parent if parent else '',
788 assert type(x[0]) in (bytes, str)
789 assert type(x[1]) in item_types
790 assert parent[0][1] == _root
791 if not S_ISDIR(item_mode(parent[-1][1])):
792 raise IOError(ENOTDIR,
793 'path resolution parent %r is not a directory'
795 is_absolute, must_be_dir, future = _decompose_path(path)
798 if not future: # path was effectively '.' or '/'
800 return notice_resolution((('', _root),))
802 return notice_resolution(tuple(parent))
803 return notice_resolution((('', _root),))
807 past = list(parent) if parent else [('', _root)]
811 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
812 raise_dir_required_but_not_dir(path, parent, past)
813 return notice_resolution(tuple(past))
814 segment = future.pop()
817 if len(past) > 1: # .. from / is /
818 assert S_ISDIR(item_mode(past[-1][1]))
821 parent_name, parent_item = past[-1]
822 wanted = (segment,) if not want_meta else ('.', segment)
823 items = tuple(contents(repo, parent_item, names=wanted,
824 want_meta=want_meta))
826 item = items[0][1] if items else None
827 else: # First item will be '.' and have the metadata
828 item = items[1][1] if len(items) == 2 else None
829 dot, dot_item = items[0]
831 past[-1] = parent_name, parent_item
833 past.append((segment, None),)
834 return notice_resolution(tuple(past))
835 mode = item_mode(item)
836 if not S_ISLNK(mode):
837 if not S_ISDIR(mode):
838 past.append((segment, item),)
840 raise IOError(ENOTDIR,
841 'path %r%s ends internally in non-directory here: %r'
843 ' (relative to %r)' % parent if parent else '',
847 raise_dir_required_but_not_dir(path, parent, past)
848 return notice_resolution(tuple(past))
850 if want_meta and type(item) in real_tree_types:
851 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
853 item = item._replace(meta=dir_meta)
854 past.append((segment, item))
856 if not future and not deref:
857 past.append((segment, item),)
861 'too many symlinks encountered while resolving %r%s'
862 % (path, ' relative to %r' % parent if parent else ''),
863 terminus=tuple(past + [(segment, item)]))
864 target = readlink(repo, item)
865 is_absolute, _, target_future = _decompose_path(target)
867 if not target_future: # path was effectively '/'
868 return notice_resolution((('', _root),))
870 future = target_future
872 future.extend(target_future)
875 def lresolve(repo, path, parent=None, want_meta=True):
876 """Perform exactly the same function as resolve(), except if the final
877 path element is a symbolic link, don't follow it, just return it
881 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
884 def resolve(repo, path, parent=None, want_meta=True):
885 """Follow the path in the virtual filesystem and return a tuple
886 representing the location, if any, denoted by the path. Each
887 element in the result tuple will be (name, info), where info will
888 be a VFS item that can be passed to functions like item_mode().
890 If a path segment that does not exist is encountered during
891 resolution, the result will represent the location of the missing
892 item, and that item in the result will be None.
894 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
897 Any symlinks along the path, including at the end, will be
898 resolved. A VFS IOError with the errno attribute set to ELOOP
899 will be raised if too many symlinks are traversed while following
900 the path. That exception is effectively like a normal
901 ELOOP IOError exception, but will include a terminus element
902 describing the location of the failure, which will be a tuple of
903 (name, info) elements.
905 The parent, if specified, must be a sequence of (name, item)
906 tuples, and will provide the starting point for the resolution of
907 the path. If no parent is specified, resolution will start at
910 The result may include elements of parent directly, so they must
911 not be modified later. If this is a concern, pass in "name,
912 copy_item(item) for name, item in parent" instead.
914 When want_meta is true, detailed metadata will be included in each
915 result item if it's avaiable, otherwise item.meta will be an
916 integer mode. The metadata size may or may not be provided, but
917 can be computed by item_size() or augment_item_meta(...,
918 include_size=True). Setting want_meta=False is rarely desirable
919 since it can limit the VFS to just the metadata git itself can
920 represent, and so, as an example, fifos and sockets will appear to
921 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
922 But the option is provided because it may be more efficient when
923 only the path names or the more limited metadata is sufficient.
925 Do not modify any item.meta Metadata instances directly. If
926 needed, make a copy via item.meta.copy() and modify that instead.
929 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
931 _, leaf_item = result[-1]
933 assert not S_ISLNK(item_mode(leaf_item))
936 def try_resolve(repo, path, parent=None, want_meta=True):
937 """If path does not refer to a symlink, does not exist, or refers to a
938 valid symlink, behave exactly like resolve(). If path refers to
939 an invalid symlink, behave like lresolve.
942 res = lresolve(repo, path, parent=parent, want_meta=want_meta)
943 leaf_name, leaf_item = res[-1]
946 if not S_ISLNK(item_mode(leaf_item)):
948 deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
949 deref_name, deref_item = deref[-1]
954 def augment_item_meta(repo, item, include_size=False):
955 """Ensure item has a Metadata instance for item.meta. If item.meta is
956 currently a mode, replace it with a compatible "fake" Metadata
957 instance. If include_size is true, ensure item.meta.size is
958 correct, computing it if needed. If item.meta is a Metadata
959 instance, this call may modify it in place or replace it.
962 # If we actually had parallelism, we'd need locking...
965 if isinstance(m, Metadata):
966 if include_size and m.size is None:
967 m.size = _compute_item_size(repo, item)
968 return item._replace(meta=m)
973 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
975 target = _readlink(repo, item.oid)
976 meta.symlink_target = target
977 meta.size = len(target)
979 meta.size = _compute_item_size(repo, item)
980 return item._replace(meta=meta)
982 def fill_in_metadata_if_dir(repo, item):
983 """If item is a directory and item.meta is not a Metadata instance,
984 attempt to find the metadata for the directory. If found, return
985 a new item augmented to include that metadata. Otherwise, return
986 item. May be useful for the output of contents().
989 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
990 items = tuple(contents(repo, item, ('.',), want_meta=True))
991 assert len(items) == 1
992 assert items[0][0] == '.'
996 def ensure_item_has_metadata(repo, item, include_size=False):
997 """If item is a directory, attempt to find and add its metadata. If
998 the item still doesn't have a Metadata instance for item.meta,
999 give it one via augment_item_meta(). May be useful for the output
1003 return augment_item_meta(repo,
1004 fill_in_metadata_if_dir(repo, item),
1005 include_size=include_size)