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):
174 count = self._compute_size() - self.ofs
175 if not self.reader or self.reader.ofs != self.ofs:
176 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
178 buf = self.reader.next(count)
181 raise # our offsets will be all screwed up otherwise
190 def __exit__(self, type, value, traceback):
194 _multiple_slashes_rx = re.compile(r'//+')
196 def _decompose_path(path):
197 """Return a boolean indicating whether the path is absolute, and a
198 reversed list of path elements, omitting any occurrences of "."
199 and ignoring any leading or trailing slash. If the path is
200 effectively '/' or '.', return an empty list.
203 path = re.sub(_multiple_slashes_rx, '/', path)
205 return True, True, []
206 is_absolute = must_be_dir = False
207 if path.startswith('/'):
210 for suffix in ('/', '/.'):
211 if path.endswith(suffix):
213 path = path[:-len(suffix)]
214 parts = [x for x in path.split('/') if x != '.']
217 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
218 return is_absolute, must_be_dir, parts
221 Item = namedtuple('Item', ('meta', 'oid'))
222 Chunky = namedtuple('Chunky', ('meta', 'oid'))
223 Root = namedtuple('Root', ('meta'))
224 Tags = namedtuple('Tags', ('meta'))
225 RevList = namedtuple('RevList', ('meta', 'oid'))
226 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
228 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
229 real_tree_types = frozenset((Item, Commit))
231 _root = Root(meta=default_dir_mode)
232 _tags = Tags(meta=default_dir_mode)
237 ### A general purpose shared cache with (currently) cheap random
238 ### eviction. At the moment there is no weighting so a single commit
239 ### item is just as likely to be evicted as an entire "rev-list". See
240 ### is_valid_cache_key for a description of the expected content.
244 _cache_max_items = 30000
247 global _cache, _cache_keys
251 def is_valid_cache_key(x):
252 """Return logically true if x looks like it could be a valid cache key
253 (with respect to structure). Current valid cache entries:
254 res:... -> resolution
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'):
266 if x.startswith('res:'):
271 assert is_valid_cache_key(key)
272 return _cache.get(key)
274 def cache_notice(key, value):
275 global _cache, _cache_keys, _cache_max_items
276 assert is_valid_cache_key(key)
279 if len(_cache) < _cache_max_items:
280 _cache_keys.append(key)
283 victim_i = randrange(0, len(_cache_keys))
284 victim = _cache_keys[victim_i]
286 _cache_keys[victim_i] = key
289 def cache_get_commit_item(oid, need_meta=True):
290 """Return the requested tree item if it can be found in the cache.
291 When need_meta is true don't return a cached item that only has a
293 # tree might be stored independently, or as '.' with its entries.
294 item = cache_get(oid)
298 if isinstance(item.meta, Metadata):
300 entries = cache_get(oid + b':r')
304 def cache_get_revlist_item(oid, need_meta=True):
305 commit = cache_get_commit_item(oid, need_meta=need_meta)
307 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)
315 if isinstance(meta, Metadata):
316 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))
735 if item_t in real_tree_types:
736 it = repo.cat(item.oid.encode('hex'))
737 _, obj_t, size = next(it)
741 # Note: it shouldn't be possible to see an Item with type
742 # 'commit' since a 'commit' should always produce a Commit.
743 raise Exception('unexpected git ' + obj_t)
745 item_gen = tree_items_with_meta(repo, item.oid, data, names)
747 item_gen = tree_items(item.oid, data, names)
748 elif item_t == RevList:
749 item_gen = revlist_items(repo, item.oid, names)
751 item_gen = root_items(repo, names, want_meta)
753 item_gen = tags_items(repo, names)
755 raise Exception('unexpected VFS item ' + str(item))
759 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
760 cache_key = b'res:%d%d%d:%s\0%s' \
761 % (bool(want_meta), bool(deref), repo.id(),
762 ('/'.join(x[0] for x in parent) if parent else ''),
764 resolution = cache_get(cache_key)
768 def notice_resolution(r):
769 cache_notice(cache_key, r)
772 def raise_dir_required_but_not_dir(path, parent, past):
773 raise IOError(ENOTDIR,
774 "path %r%s resolves to non-directory %r"
776 ' (relative to %r)' % parent if parent else '',
785 assert type(x[0]) in (bytes, str)
786 assert type(x[1]) in item_types
787 assert parent[0][1] == _root
788 if not S_ISDIR(item_mode(parent[-1][1])):
789 raise IOError(ENOTDIR,
790 'path resolution parent %r is not a directory'
792 is_absolute, must_be_dir, future = _decompose_path(path)
795 if not future: # path was effectively '.' or '/'
797 return notice_resolution((('', _root),))
799 return notice_resolution(tuple(parent))
800 return notice_resolution((('', _root),))
804 past = list(parent) if parent else [('', _root)]
808 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
809 raise_dir_required_but_not_dir(path, parent, past)
810 return notice_resolution(tuple(past))
811 segment = future.pop()
814 if len(past) > 1: # .. from / is /
815 assert S_ISDIR(item_mode(past[-1][1]))
818 parent_name, parent_item = past[-1]
819 wanted = (segment,) if not want_meta else ('.', segment)
820 items = tuple(contents(repo, parent_item, names=wanted,
821 want_meta=want_meta))
823 item = items[0][1] if items else None
824 else: # First item will be '.' and have the metadata
825 item = items[1][1] if len(items) == 2 else None
826 dot, dot_item = items[0]
828 past[-1] = parent_name, parent_item
830 past.append((segment, None),)
831 return notice_resolution(tuple(past))
832 mode = item_mode(item)
833 if not S_ISLNK(mode):
834 if not S_ISDIR(mode):
835 past.append((segment, item),)
837 raise IOError(ENOTDIR,
838 'path %r%s ends internally in non-directory here: %r'
840 ' (relative to %r)' % parent if parent else '',
844 raise_dir_required_but_not_dir(path, parent, past)
845 return notice_resolution(tuple(past))
847 if want_meta and type(item) in real_tree_types:
848 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
850 item = item._replace(meta=dir_meta)
851 past.append((segment, item))
853 if not future and not deref:
854 past.append((segment, item),)
858 'too many symlinks encountered while resolving %r%s'
859 % (path, ' relative to %r' % parent if parent else ''),
860 terminus=tuple(past + [(segment, item)]))
861 target = readlink(repo, item)
862 is_absolute, _, target_future = _decompose_path(target)
864 if not target_future: # path was effectively '/'
865 return notice_resolution((('', _root),))
867 future = target_future
869 future.extend(target_future)
872 def lresolve(repo, path, parent=None, want_meta=True):
873 """Perform exactly the same function as resolve(), except if the final
874 path element is a symbolic link, don't follow it, just return it
878 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
881 def resolve(repo, path, parent=None, want_meta=True):
882 """Follow the path in the virtual filesystem and return a tuple
883 representing the location, if any, denoted by the path. Each
884 element in the result tuple will be (name, info), where info will
885 be a VFS item that can be passed to functions like item_mode().
887 If a path segment that does not exist is encountered during
888 resolution, the result will represent the location of the missing
889 item, and that item in the result will be None.
891 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
894 Any symlinks along the path, including at the end, will be
895 resolved. A VFS IOError with the errno attribute set to ELOOP
896 will be raised if too many symlinks are traversed while following
897 the path. That exception is effectively like a normal
898 ELOOP IOError exception, but will include a terminus element
899 describing the location of the failure, which will be a tuple of
900 (name, info) elements.
902 The parent, if specified, must be a sequence of (name, item)
903 tuples, and will provide the starting point for the resolution of
904 the path. If no parent is specified, resolution will start at
907 The result may include elements of parent directly, so they must
908 not be modified later. If this is a concern, pass in "name,
909 copy_item(item) for name, item in parent" instead.
911 When want_meta is true, detailed metadata will be included in each
912 result item if it's avaiable, otherwise item.meta will be an
913 integer mode. The metadata size may or may not be provided, but
914 can be computed by item_size() or augment_item_meta(...,
915 include_size=True). Setting want_meta=False is rarely desirable
916 since it can limit the VFS to just the metadata git itself can
917 represent, and so, as an example, fifos and sockets will appear to
918 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
919 But the option is provided because it may be more efficient when
920 only the path names or the more limited metadata is sufficient.
922 Do not modify any item.meta Metadata instances directly. If
923 needed, make a copy via item.meta.copy() and modify that instead.
926 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
928 _, leaf_item = result[-1]
930 assert not S_ISLNK(item_mode(leaf_item))
933 def try_resolve(repo, path, parent=None, want_meta=True):
934 """If path does not refer to a symlink, does not exist, or refers to a
935 valid symlink, behave exactly like resolve(). If path refers to
936 an invalid symlink, behave like lresolve.
939 res = lresolve(repo, path, parent=parent, want_meta=want_meta)
940 leaf_name, leaf_item = res[-1]
943 if not S_ISLNK(item_mode(leaf_item)):
945 deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
946 deref_name, deref_item = deref[-1]
951 def augment_item_meta(repo, item, include_size=False):
952 """Ensure item has a Metadata instance for item.meta. If item.meta is
953 currently a mode, replace it with a compatible "fake" Metadata
954 instance. If include_size is true, ensure item.meta.size is
955 correct, computing it if needed. If item.meta is a Metadata
956 instance, this call may modify it in place or replace it.
959 # If we actually had parallelism, we'd need locking...
962 if isinstance(m, Metadata):
963 if include_size and m.size is None:
964 m.size = _compute_item_size(repo, item)
965 return item._replace(meta=m)
970 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
972 target = _readlink(repo, item.oid)
973 meta.symlink_target = target
974 meta.size = len(target)
976 meta.size = _compute_item_size(repo, item)
977 return item._replace(meta=meta)
979 def fill_in_metadata_if_dir(repo, item):
980 """If item is a directory and item.meta is not a Metadata instance,
981 attempt to find the metadata for the directory. If found, return
982 a new item augmented to include that metadata. Otherwise, return
983 item. May be useful for the output of contents().
986 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
987 items = tuple(contents(repo, item, ('.',), want_meta=True))
988 assert len(items) == 1
989 assert items[0][0] == '.'
993 def ensure_item_has_metadata(repo, item, include_size=False):
994 """If item is a directory, attempt to find and add its metadata. If
995 the item still doesn't have a Metadata instance for item.meta,
996 give it one via augment_item_meta(). May be useful for the output
1000 return augment_item_meta(repo,
1001 fill_in_metadata_if_dir(repo, item),
1002 include_size=include_size)