1 """Virtual File System interface to bup repository content.
3 This module provides a path-based interface to the content of a bup
6 The VFS is structured like this:
9 /SAVE-NAME/SAVE-DATE/...
12 Each path is represented by an item that has least an item.meta which
13 may be either a Metadata object, or an integer mode. Functions like
14 item_mode() and item_size() will return the mode and size in either
15 case. Any item.meta Metadata instances must not be modified directly.
16 Make a copy to modify via item.meta.copy() if needed.
18 The want_meta argument is advisory for calls that accept it, and it
19 may not be honored. Callers must be able to handle an item.meta value
20 that is either an instance of Metadata or an integer mode, perhaps
21 via item_mode() or augment_item_meta().
23 Setting want_meta=False is rarely desirable since it can limit the VFS
24 to only the metadata that git itself can represent, and so for
25 example, fifos and sockets will appear to be regular files
26 (e.g. S_ISREG(item_mode(item)) will be true). But the option is still
27 provided because it may be more efficient when just the path names or
28 the more limited metadata is sufficient.
30 Any given metadata object's size may be None, in which case the size
31 can be computed via item_size() or augment_item_meta(...,
34 When traversing a directory using functions like contents(), the meta
35 value for any directories other than '.' will be a default directory
36 mode, not a Metadata object. This is because the actual metadata for
37 a directory is stored inside the directory (see
38 fill_in_metadata_if_dir() or ensure_item_has_metadata()).
40 Commit items represent commits (e.g. /.tag/some-commit or
41 /foo/latest), and for most purposes, they appear as the underlying
42 tree. S_ISDIR(item_mode(item)) will return true for both tree Items
43 and Commits and the commit's oid is the tree hash; the commit hash is
48 from __future__ import print_function
49 from collections import namedtuple
50 from errno import ELOOP, ENOENT, ENOTDIR
51 from itertools import chain, dropwhile, groupby, izip, tee
52 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
53 from time import localtime, strftime
54 import exceptions, re, sys
56 from bup import client, git, metadata
57 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
58 from bup.helpers import debug2, last
59 from bup.metadata import Metadata
60 from bup.repo import LocalRepo, RemoteRepo
63 class IOError(exceptions.IOError):
64 def __init__(self, errno, message, terminus=None):
65 exceptions.IOError.__init__(self, errno, message)
66 self.terminus = terminus
68 default_file_mode = S_IFREG | 0o644
69 default_dir_mode = S_IFDIR | 0o755
70 default_symlink_mode = S_IFLNK | 0o755
72 def _default_mode_for_gitmode(gitmode):
74 return default_file_mode
76 return default_dir_mode
78 return default_symlink_mode
79 raise Exception('unexpected git mode ' + oct(gitmode))
81 def _normal_or_chunked_file_size(repo, oid):
82 """Return the size of the normal or chunked file indicated by oid."""
83 # FIXME: --batch-format CatPipe?
84 it = repo.cat(oid.encode('hex'))
85 _, obj_t, size = next(it)
87 while obj_t == 'tree':
88 mode, name, last_oid = last(tree_decode(''.join(it)))
90 it = repo.cat(last_oid.encode('hex'))
91 _, obj_t, size = next(it)
92 return ofs + sum(len(b) for b in it)
94 def _tree_chunks(repo, tree, startofs):
95 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
97 # name is the chunk's hex offset in the original file
98 tree = dropwhile(lambda (_1, name, _2): int(name, 16) < startofs, tree)
99 for mode, name, oid in tree:
101 skipmore = startofs - ofs
104 it = repo.cat(oid.encode('hex'))
105 _, obj_t, size = next(it)
108 assert obj_t == 'tree'
109 for b in _tree_chunks(repo, tree_decode(data), skipmore):
112 assert obj_t == 'blob'
113 yield data[skipmore:]
116 def __init__(self, repo, oid, startofs):
117 it = repo.cat(oid.encode('hex'))
118 _, obj_t, size = next(it)
119 isdir = obj_t == 'tree'
122 self.it = _tree_chunks(repo, tree_decode(data), startofs)
126 self.blob = data[startofs:]
129 def next(self, size):
131 while len(out) < size:
132 if self.it and not self.blob:
134 self.blob = self.it.next()
135 except StopIteration:
138 want = size - len(out)
139 out += self.blob[:want]
140 self.blob = self.blob[want:]
143 debug2('next(%d) returned %d\n' % (size, len(out)))
147 class _FileReader(object):
148 def __init__(self, repo, oid, known_size=None):
149 assert len(oid) == 20
154 self._size = known_size
156 def _compute_size(self):
158 self._size = _normal_or_chunked_file_size(self._repo, self.oid)
163 raise IOError(errno.EINVAL, 'Invalid argument')
164 if ofs > self._compute_size():
165 raise IOError(errno.EINVAL, 'Invalid argument')
171 def read(self, count=-1):
173 count = self._compute_size() - self.ofs
174 if not self.reader or self.reader.ofs != self.ofs:
175 self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
177 buf = self.reader.next(count)
180 raise # our offsets will be all screwed up otherwise
189 def __exit__(self, type, value, traceback):
193 _multiple_slashes_rx = re.compile(r'//+')
195 def _decompose_path(path):
196 """Return a boolean indicating whether the path is absolute, and a
197 reversed list of path elements, omitting any occurrences of "."
198 and ignoring any leading or trailing slash. If the path is
199 effectively '/' or '.', return an empty list.
202 path = re.sub(_multiple_slashes_rx, '/', path)
204 return True, True, []
205 is_absolute = must_be_dir = False
206 if path.startswith('/'):
209 for suffix in ('/', '/.'):
210 if path.endswith(suffix):
212 path = path[:-len(suffix)]
213 parts = [x for x in path.split('/') if x != '.']
216 must_be_dir = True # e.g. path was effectively '.' or '/', etc.
217 return is_absolute, must_be_dir, parts
220 Item = namedtuple('Item', ('meta', 'oid'))
221 Chunky = namedtuple('Chunky', ('meta', 'oid'))
222 Root = namedtuple('Root', ('meta'))
223 Tags = namedtuple('Tags', ('meta'))
224 RevList = namedtuple('RevList', ('meta', 'oid'))
225 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
227 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
228 real_tree_types = frozenset((Item, Commit))
230 _root = Root(meta=default_dir_mode)
231 _tags = Tags(meta=default_dir_mode)
236 ### A general purpose shared cache with (currently) cheap random
237 ### eviction. There is currently no weighting so a single commit item
238 ### is just as likely to be evicted as an entire "rev-list". See
239 ### is_valid_cache_key for a description of the expected content.
243 _cache_max_items = 30000
246 global _cache, _cache_keys
250 def is_valid_cache_key(x):
251 """Return logically true if x looks like it could be a valid cache key
252 (with respect to structure). Current valid cache entries:
254 commit_oid + ':r' -> rev-list
255 i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
257 # Suspect we may eventually add "(container_oid, name) -> ...", and others.
262 if len(x) == 22 and x.endswith(b':r'):
267 assert is_valid_cache_key(key)
268 return _cache.get(key)
270 def cache_notice(key, value):
271 global _cache, _cache_keys, _cache_max_items
272 assert is_valid_cache_key(key)
276 if len(_cache) < _cache_max_items:
278 victim_i = random.randrange(0, len(_cache_keys))
279 victim = _cache_keys[victim_i]
280 _cache_keys[victim_i] = key
284 def cache_get_commit_item(oid, need_meta=True):
285 """Return the requested tree item if it can be found in the cache.
286 When need_meta is true don't return a cached item that only has a
288 # tree might be stored independently, or as '.' with its entries.
289 item = cache_get(oid)
293 if isinstance(item.meta, Metadata):
295 entries = cache_get(oid + b':r')
299 def cache_get_revlist_item(oid, need_meta=True):
300 commit = cache_get_commit_item(oid, need_meta=need_meta)
302 return RevList(oid=oid, meta=commit.meta)
306 """Return a completely independent copy of item, such that
307 modifications will not affect the original.
310 meta = getattr(item, 'meta', None)
313 return(item._replace(meta=meta.copy()))
316 """Return the integer mode (stat st_mode) for item."""
318 if isinstance(m, Metadata):
322 def _read_dir_meta(bupm):
323 # This is because save writes unmodified Metadata() entries for
324 # fake parents -- test-save-strip-graft.sh demonstrates.
325 m = Metadata.read(bupm)
327 return default_dir_mode
328 assert m.mode is not None
333 def tree_data_and_bupm(repo, oid):
334 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
335 tree has no metadata (i.e. older bup save, or non-bup tree).
338 assert len(oid) == 20
339 it = repo.cat(oid.encode('hex'))
340 _, item_t, size = next(it)
342 if item_t == 'commit':
343 commit = parse_commit(data)
344 it = repo.cat(commit.tree)
345 _, item_t, size = next(it)
347 assert item_t == 'tree'
348 elif item_t != 'tree':
349 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
350 for _, mangled_name, sub_oid in tree_decode(data):
351 if mangled_name == '.bupm':
353 if mangled_name > '.bupm':
357 def _find_treeish_oid_metadata(repo, oid):
358 """Return the metadata for the tree or commit oid, or None if the tree
359 has no metadata (i.e. older bup save, or non-bup tree).
362 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
364 with _FileReader(repo, bupm_oid) as meta_stream:
365 return _read_dir_meta(meta_stream)
368 def _readlink(repo, oid):
369 return ''.join(repo.join(oid.encode('hex')))
371 def readlink(repo, item):
372 """Return the link target of item, which must be a symlink. Reads the
373 target from the repository if necessary."""
375 assert S_ISLNK(item_mode(item))
376 if isinstance(item.meta, Metadata):
377 target = item.meta.symlink_target
380 return _readlink(repo, item.oid)
382 def _compute_item_size(repo, item):
383 mode = item_mode(item)
385 size = _normal_or_chunked_file_size(repo, item.oid)
388 return len(_readlink(repo, item.oid))
391 def item_size(repo, item):
392 """Return the size of item, computing it if necessary."""
394 if isinstance(m, Metadata) and m.size is not None:
396 return _compute_item_size(repo, item)
398 def tree_data_reader(repo, oid):
399 """Return an open reader for all of the data contained within oid. If
400 oid refers to a tree, recursively concatenate all of its contents."""
401 return _FileReader(repo, oid)
403 def fopen(repo, item):
404 """Return an open reader for the given file item."""
405 assert S_ISREG(item_mode(item))
406 return tree_data_reader(repo, item.oid)
408 def _commit_item_from_data(oid, data):
409 info = parse_commit(data)
410 return Commit(meta=default_dir_mode,
411 oid=info.tree.decode('hex'),
414 def _commit_item_from_oid(repo, oid, require_meta):
415 commit = cache_get_commit_item(oid, need_meta=require_meta)
416 if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
418 it = repo.cat(oid.encode('hex'))
419 _, typ, size = next(it)
420 assert typ == 'commit'
421 commit = _commit_item_from_data(oid, ''.join(it))
423 meta = _find_treeish_oid_metadata(repo, commit.oid)
425 commit = commit._replace(meta=meta)
426 cache_notice(oid, commit)
429 def _revlist_item_from_oid(repo, oid, require_meta):
430 commit = _commit_item_from_oid(repo, oid, require_meta)
431 return RevList(oid=oid, meta=commit.meta)
433 def root_items(repo, names=None, want_meta=True):
434 """Yield (name, item) for the items in '/' in the VFS. Return
435 everything if names is logically false, otherwise return only
436 items with a name in the collection.
439 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
445 # FIXME: maybe eventually support repo.clone() or something
446 # and pass in two repos, so we can drop the tuple() and stream
447 # in parallel (i.e. meta vs refs).
448 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
449 assert(name.startswith('refs/heads/'))
450 yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
458 if ref in ('.', '.tag'):
461 oidx, typ, size = next(it)
465 assert typ == 'commit'
466 commit = parse_commit(''.join(it))
467 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
469 def ordered_tree_entries(tree_data, bupm=None):
470 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
471 tree, sorted by name.
474 # Sadly, the .bupm entries currently aren't in git tree order,
475 # i.e. they don't account for the fact that git sorts trees
476 # (including our chunked trees) as if their names ended with "/",
477 # so "fo" sorts after "fo." iff fo is a directory. This makes
478 # streaming impossible when we need the metadata.
479 def result_from_tree_entry(tree_entry):
480 gitmode, mangled_name, oid = tree_entry
481 name, kind = git.demangle_name(mangled_name, gitmode)
482 return name, mangled_name, kind, gitmode, oid
484 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
486 tree_ents = sorted(tree_ents, key=lambda x: x[0])
487 for ent in tree_ents:
490 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
492 def tree_item(ent_oid, kind, gitmode):
493 if kind == BUP_CHUNKED:
494 meta = Metadata.read(bupm) if bupm else default_file_mode
495 return Chunky(oid=ent_oid, meta=meta)
498 # No metadata here (accessable via '.' inside ent_oid).
499 return Item(meta=default_dir_mode, oid=ent_oid)
501 return Item(oid=ent_oid,
502 meta=(Metadata.read(bupm) if bupm \
503 else _default_mode_for_gitmode(gitmode)))
505 assert len(oid) == 20
507 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
508 yield '.', Item(oid=oid, meta=dot_meta)
509 tree_entries = ordered_tree_entries(tree_data, bupm)
510 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
511 if mangled_name == '.bupm':
514 yield name, tree_item(ent_oid, kind, gitmode)
517 # Assumes the tree is properly formed, i.e. there are no
518 # duplicates, and entries will be in git tree order.
519 if type(names) not in (frozenset, set):
520 names = frozenset(names)
521 remaining = len(names)
523 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
524 last_name = max(names) if bupm else max(names) + '/'
527 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
528 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 if name not in names:
540 break # given bupm sort order, we're finished
541 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
544 yield name, tree_item(ent_oid, kind, gitmode)
549 def tree_items_with_meta(repo, oid, tree_data, names):
550 # For now, the .bupm order doesn't quite match git's, and we don't
551 # load the tree data incrementally anyway, so we just work in RAM
553 assert len(oid) == 20
555 for _, mangled_name, sub_oid in tree_decode(tree_data):
556 if mangled_name == '.bupm':
557 bupm = _FileReader(repo, sub_oid)
559 if mangled_name > '.bupm':
561 for item in tree_items(oid, tree_data, names, bupm):
564 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
566 def _reverse_suffix_duplicates(strs):
567 """Yields the elements of strs, with any runs of duplicate values
568 suffixed with -N suffixes, where the zero padded integer N
569 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
572 for name, duplicates in groupby(strs):
573 ndup = len(tuple(duplicates))
577 ndig = len(str(ndup - 1))
578 fmt = '%s-' + '%0' + str(ndig) + 'd'
579 for i in xrange(ndup - 1, -1, -1):
580 yield fmt % (name, i)
583 items = f.readline().split(None)
584 assert len(items) == 2
585 tree, auth_sec = items
586 return tree.decode('hex'), int(auth_sec)
588 def _name_for_rev(rev):
589 commit_oidx, (tree_oid, utc) = rev
590 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
592 def _item_for_rev(rev):
593 commit_oidx, (tree_oid, utc) = rev
594 coid = commit_oidx.decode('hex')
595 item = cache_get_commit_item(coid, need_meta=False)
598 item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
599 cache_notice(item.coid, item)
602 def cache_commit(repo, oid):
603 """Build, cache, and return a "name -> commit_item" dict of the entire
607 # For now, always cache with full metadata
609 entries['.'] = _revlist_item_from_oid(repo, oid, True)
610 revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
612 rev_items, rev_names = tee(revs)
613 revs = None # Don't disturb the tees
614 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
615 rev_items = (_item_for_rev(x) for x in rev_items)
617 for item in rev_items:
618 latest = latest or item
619 name = next(rev_names)
621 entries['latest'] = latest
622 cache_notice(latest.coid + b':r', entries)
625 def revlist_items(repo, oid, names):
626 assert len(oid) == 20
628 # Special case '.' instead of caching the whole history since it's
629 # the only way to get the metadata for the commit.
630 if names and all(x == '.' for x in names):
631 yield '.', _revlist_item_from_oid(repo, oid, True)
634 # For now, don't worry about the possibility of the contents being
635 # "too big" for the cache.
636 entries = cache_get(oid + b':r')
638 entries = cache_commit(repo, oid)
641 for name in sorted(entries.keys()):
642 yield name, entries[name]
645 names = frozenset(name for name in names
646 if _save_name_rx.match(name) or name in ('.', 'latest'))
649 yield '.', entries['.']
650 for name in (n for n in names if n != '.'):
651 commit = entries.get(name)
655 def tags_items(repo, names):
659 assert len(oid) == 20
660 oidx = oid.encode('hex')
662 _, typ, size = next(it)
664 return cache_get_commit_item(oid, need_meta=False) \
665 or _commit_item_from_data(oid, ''.join(it))
668 return Item(meta=default_file_mode, oid=oid)
670 return Item(meta=default_dir_mode, oid=oid)
671 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
675 # We have to pull these all into ram because tag_item calls cat()
676 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
677 assert(name.startswith('refs/tags/'))
679 yield name, tag_item(oid)
682 # Assumes no duplicate refs
683 if type(names) not in (frozenset, set):
684 names = frozenset(names)
685 remaining = len(names)
686 last_name = max(names)
693 for name, oid in repo.refs(names, limit_to_tags=True):
694 assert(name.startswith('refs/tags/'))
698 if name not in names:
700 yield name, tag_item(oid)
705 def contents(repo, item, names=None, want_meta=True):
706 """Yields information about the items contained in item. Yields
707 (name, item) for each name in names, if the name exists, in an
708 unspecified order. If there are no names, then yields (name,
709 item) for all items, including, a first item named '.'
710 representing the container itself.
712 The meta value for any directories other than '.' will be a
713 default directory mode, not a Metadata object. This is because
714 the actual metadata for a directory is stored inside the directory
715 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
717 Note that want_meta is advisory. For any given item, item.meta
718 might be a Metadata instance or a mode, and if the former,
719 meta.size might be None. Missing sizes can be computed via via
720 item_size() or augment_item_meta(..., include_size=True).
722 Do not modify any item.meta Metadata instances directly. If
723 needed, make a copy via item.meta.copy() and modify that instead.
726 # Q: are we comfortable promising '.' first when no names?
729 assert S_ISDIR(item_mode(item))
732 if item_t in real_tree_types:
733 it = repo.cat(item.oid.encode('hex'))
734 _, obj_type, size = next(it)
736 if obj_type == 'tree':
738 item_gen = tree_items_with_meta(repo, item.oid, data, names)
740 item_gen = tree_items(item.oid, data, names)
741 elif obj_type == 'commit':
743 item_gen = tree_items_with_meta(repo, item.oid, tree_data, names)
745 item_gen = tree_items(item.oid, tree_data, names)
748 raise Exception('unexpected git ' + obj_type)
749 elif item_t == RevList:
750 item_gen = revlist_items(repo, item.oid, names)
752 item_gen = root_items(repo, names, want_meta)
754 item_gen = tags_items(repo, names)
756 raise Exception('unexpected VFS item ' + str(item))
760 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
761 def raise_dir_required_but_not_dir(path, parent, past):
762 raise IOError(ENOTDIR,
763 "path %r%s resolves to non-directory %r"
765 ' (relative to %r)' % parent if parent else '',
774 assert type(x[0]) in (bytes, str)
775 assert type(x[1]) in item_types
776 assert parent[0][1] == _root
777 if not S_ISDIR(item_mode(parent[-1][1])):
778 raise IOError(ENOTDIR,
779 'path resolution parent %r is not a directory'
781 is_absolute, must_be_dir, future = _decompose_path(path)
784 if not future: # path was effectively '.' or '/'
786 return (('', _root),)
793 past = list(parent) if parent else [('', _root)]
797 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
798 raise_dir_required_but_not_dir(path, parent, past)
800 segment = future.pop()
803 if len(past) > 1: # .. from / is /
804 assert S_ISDIR(item_mode(past[-1][1]))
807 parent_name, parent_item = past[-1]
808 wanted = (segment,) if not want_meta else ('.', segment)
809 items = tuple(contents(repo, parent_item, names=wanted,
810 want_meta=want_meta))
812 item = items[0][1] if items else None
813 else: # First item will be '.' and have the metadata
814 item = items[1][1] if len(items) == 2 else None
815 dot, dot_item = items[0]
817 past[-1] = parent_name, parent_item
819 past.append((segment, None),)
821 mode = item_mode(item)
822 if not S_ISLNK(mode):
823 if not S_ISDIR(mode):
824 past.append((segment, item),)
826 raise IOError(ENOTDIR,
827 'path %r%s ends internally in non-directory here: %r'
829 ' (relative to %r)' % parent if parent else '',
833 raise_dir_required_but_not_dir(path, parent, past)
836 if want_meta and type(item) in real_tree_types:
837 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
839 item = item._replace(meta=dir_meta)
840 past.append((segment, item))
842 if not future and not deref:
843 past.append((segment, item),)
847 'too many symlinks encountered while resolving %r%s'
848 % (path, ' relative to %r' % parent if parent else ''),
849 terminus=tuple(past + [(segment, item)]))
850 target = readlink(repo, item)
851 is_absolute, _, target_future = _decompose_path(target)
853 if not target_future: # path was effectively '/'
854 return (('', _root),)
856 future = target_future
858 future.extend(target_future)
861 def lresolve(repo, path, parent=None, want_meta=True):
862 """Perform exactly the same function as resolve(), except if the final
863 path element is a symbolic link, don't follow it, just return it
867 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
870 def resolve(repo, path, parent=None, want_meta=True):
871 """Follow the path in the virtual filesystem and return a tuple
872 representing the location, if any, denoted by the path. Each
873 element in the result tuple will be (name, info), where info will
874 be a VFS item that can be passed to functions like item_mode().
876 If a path segment that does not exist is encountered during
877 resolution, the result will represent the location of the missing
878 item, and that item in the result will be None.
880 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
883 Any symlinks along the path, including at the end, will be
884 resolved. A VFS IOError with the errno attribute set to ELOOP
885 will be raised if too many symlinks are traversed while following
886 the path. That exception is effectively like a normal
887 ELOOP IOError exception, but will include a terminus element
888 describing the location of the failure, which will be a tuple of
889 (name, info) elements.
891 The parent, if specified, must be a sequence of (name, item)
892 tuples, and will provide the starting point for the resolution of
893 the path. If no parent is specified, resolution will start at
896 The result may include elements of parent directly, so they must
897 not be modified later. If this is a concern, pass in "name,
898 copy_item(item) for name, item in parent" instead.
900 When want_meta is true, detailed metadata will be included in each
901 result item if it's avaiable, otherwise item.meta will be an
902 integer mode. The metadata size may or may not be provided, but
903 can be computed by item_size() or augment_item_meta(...,
904 include_size=True). Setting want_meta=False is rarely desirable
905 since it can limit the VFS to just the metadata git itself can
906 represent, and so, as an example, fifos and sockets will appear to
907 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
908 But the option is provided because it may be more efficient when
909 only the path names or the more limited metadata is sufficient.
911 Do not modify any item.meta Metadata instances directly. If
912 needed, make a copy via item.meta.copy() and modify that instead.
915 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
917 _, leaf_item = result[-1]
919 assert not S_ISLNK(item_mode(leaf_item))
922 def try_resolve(repo, path, parent=None, want_meta=True):
923 """If path does not refer to a symlink, does not exist, or refers to a
924 valid symlink, behave exactly like resolve(). If path refers to
925 an invalid symlink, behave like lresolve.
928 res = lresolve(repo, path, parent=parent, want_meta=want_meta)
929 leaf_name, leaf_item = res[-1]
932 if not S_ISLNK(item_mode(leaf_item)):
934 deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
935 deref_name, deref_item = deref[-1]
940 def augment_item_meta(repo, item, include_size=False):
941 """Ensure item has a Metadata instance for item.meta. If item.meta is
942 currently a mode, replace it with a compatible "fake" Metadata
943 instance. If include_size is true, ensure item.meta.size is
944 correct, computing it if needed. If item.meta is a Metadata
945 instance, this call may modify it in place or replace it.
948 # If we actually had parallelism, we'd need locking...
951 if isinstance(m, Metadata):
952 if include_size and m.size is None:
953 m.size = _compute_item_size(repo, item)
954 return item._replace(meta=m)
959 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
961 target = _readlink(repo, item.oid)
962 meta.symlink_target = target
963 meta.size = len(target)
965 meta.size = _compute_item_size(repo, item)
966 return item._replace(meta=meta)
968 def fill_in_metadata_if_dir(repo, item):
969 """If item is a directory and item.meta is not a Metadata instance,
970 attempt to find the metadata for the directory. If found, return
971 a new item augmented to include that metadata. Otherwise, return
972 item. May be useful for the output of contents().
975 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
976 items = tuple(contents(repo, item, ('.',), want_meta=True))
977 assert len(items) == 1
978 assert items[0][0] == '.'
982 def ensure_item_has_metadata(repo, item, include_size=False):
983 """If item is a directory, attempt to find and add its metadata. If
984 the item still doesn't have a Metadata instance for item.meta,
985 give it one via augment_item_meta(). May be useful for the output
989 return augment_item_meta(repo,
990 fill_in_metadata_if_dir(repo, item),
991 include_size=include_size)