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
44 will be item.coid, and nominal_oid(item) will return coid for commits,
45 oid for everything else.
49 from __future__ import print_function
50 from collections import namedtuple
51 from errno import ELOOP, ENOENT, ENOTDIR
52 from itertools import chain, dropwhile, groupby, izip, tee
53 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
54 from time import localtime, strftime
55 import exceptions, re, sys
57 from bup import client, git, metadata
58 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
59 from bup.helpers import debug2, last
60 from bup.metadata import Metadata
61 from bup.repo import LocalRepo, RemoteRepo
64 class IOError(exceptions.IOError):
65 def __init__(self, errno, message, terminus=None):
66 exceptions.IOError.__init__(self, errno, message)
67 self.terminus = terminus
69 default_file_mode = S_IFREG | 0o644
70 default_dir_mode = S_IFDIR | 0o755
71 default_symlink_mode = S_IFLNK | 0o755
73 def _default_mode_for_gitmode(gitmode):
75 return default_file_mode
77 return default_dir_mode
79 return default_symlink_mode
80 raise Exception('unexpected git mode ' + oct(gitmode))
82 def _normal_or_chunked_file_size(repo, oid):
83 """Return the size of the normal or chunked file indicated by oid."""
84 # FIXME: --batch-format CatPipe?
85 it = repo.cat(oid.encode('hex'))
86 _, obj_t, size = next(it)
88 while obj_t == 'tree':
89 mode, name, last_oid = last(tree_decode(''.join(it)))
91 it = repo.cat(last_oid.encode('hex'))
92 _, obj_t, size = next(it)
93 return ofs + sum(len(b) for b in it)
95 def _tree_chunks(repo, tree, startofs):
96 "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
98 # name is the chunk's hex offset in the original file
99 tree = dropwhile(lambda (_1, name, _2): int(name, 16) < startofs, tree)
100 for mode, name, oid in tree:
102 skipmore = startofs - ofs
105 it = repo.cat(oid.encode('hex'))
106 _, obj_t, size = next(it)
109 assert obj_t == 'tree'
110 for b in _tree_chunks(repo, tree_decode(data), skipmore):
113 assert obj_t == 'blob'
114 yield data[skipmore:]
117 def __init__(self, repo, oid, startofs):
118 it = repo.cat(oid.encode('hex'))
119 _, obj_t, size = next(it)
120 isdir = obj_t == 'tree'
123 self.it = _tree_chunks(repo, tree_decode(data), startofs)
127 self.blob = data[startofs:]
130 def next(self, size):
132 while len(out) < size:
133 if self.it and not self.blob:
135 self.blob = self.it.next()
136 except StopIteration:
139 want = size - len(out)
140 out += self.blob[:want]
141 self.blob = self.blob[want:]
144 debug2('next(%d) returned %d\n' % (size, len(out)))
148 class _FileReader(object):
149 def __init__(self, repo, oid, known_size=None):
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)
234 def nominal_oid(item):
235 """If the item is a Commit, return its commit oid, otherwise return
236 the item's oid, if it has one.
239 if isinstance(item, Commit):
241 return getattr(item, 'oid', None)
244 """Return a completely independent copy of item, such that
245 modifications will not affect the original.
248 meta = getattr(item, 'meta', None)
251 return(item._replace(meta=meta.copy()))
254 """Return the integer mode (stat st_mode) for item."""
256 if isinstance(m, Metadata):
260 def _read_dir_meta(bupm):
261 # This is because save writes unmodified Metadata() entries for
262 # fake parents -- test-save-strip-graft.sh demonstrates.
263 m = Metadata.read(bupm)
265 return default_dir_mode
266 assert m.mode is not None
271 def tree_data_and_bupm(repo, oid):
272 """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
273 tree has no metadata (i.e. older bup save, or non-bup tree).
276 assert len(oid) == 20
277 it = repo.cat(oid.encode('hex'))
278 _, item_t, size = next(it)
280 if item_t == 'commit':
281 commit = parse_commit(data)
282 it = repo.cat(commit.tree)
283 _, item_t, size = next(it)
285 assert item_t == 'tree'
286 elif item_t != 'tree':
287 raise Exception('%r is not a tree or commit' % oid.encode('hex'))
288 for _, mangled_name, sub_oid in tree_decode(data):
289 if mangled_name == '.bupm':
291 if mangled_name > '.bupm':
295 def _find_treeish_oid_metadata(repo, oid):
296 """Return the metadata for the tree or commit oid, or None if the tree
297 has no metadata (i.e. older bup save, or non-bup tree).
300 tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
302 with _FileReader(repo, bupm_oid) as meta_stream:
303 return _read_dir_meta(meta_stream)
306 def _readlink(repo, oid):
307 return ''.join(repo.join(oid.encode('hex')))
309 def readlink(repo, item):
310 """Return the link target of item, which must be a symlink. Reads the
311 target from the repository if necessary."""
313 assert S_ISLNK(item_mode(item))
314 if isinstance(item.meta, Metadata):
315 target = item.meta.symlink_target
318 return _readlink(repo, item.oid)
320 def _compute_item_size(repo, item):
321 mode = item_mode(item)
323 size = _normal_or_chunked_file_size(repo, item.oid)
326 return len(_readlink(repo, item.oid))
329 def item_size(repo, item):
330 """Return the size of item, computing it if necessary."""
332 if isinstance(m, Metadata) and m.size is not None:
334 return _compute_item_size(repo, item)
336 def fopen(repo, item):
337 """Return an open reader for the given file item."""
339 assert S_ISREG(item_mode(item))
340 return _FileReader(repo, item.oid)
342 def _commit_item_from_data(oid, data):
343 info = parse_commit(data)
344 return Commit(meta=default_dir_mode,
345 oid=info.tree.decode('hex'),
348 def _commit_item_from_oid(repo, oid, require_meta):
349 it = repo.cat(oid.encode('hex'))
350 _, typ, size = next(it)
351 assert typ == 'commit'
352 commit = _commit_item_from_data(oid, ''.join(it))
354 meta = _find_treeish_oid_metadata(repo, commit.tree)
356 commit = commit._replace(meta=meta)
359 def _revlist_item_from_oid(repo, oid, require_meta):
361 meta = _find_treeish_oid_metadata(repo, oid) or default_dir_mode
363 meta = default_dir_mode
364 return RevList(oid=oid, meta=meta)
366 def parse_rev_auth_secs(f):
367 tree, author_secs = f.readline().split(None, 2)
368 return tree, int(author_secs)
370 def root_items(repo, names=None):
371 """Yield (name, item) for the items in '/' in the VFS. Return
372 everything if names is logically false, otherwise return only
373 items with a name in the collection.
376 # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
382 # FIXME: maybe eventually support repo.clone() or something
383 # and pass in two repos, so we can drop the tuple() and stream
384 # in parallel (i.e. meta vs refs).
385 for name, oid in tuple(repo.refs([], limit_to_heads=True)):
386 assert(name.startswith('refs/heads/'))
387 yield name[11:], _revlist_item_from_oid(repo, oid, False)
395 if ref in ('.', '.tag'):
398 oidx, typ, size = next(it)
402 assert typ == 'commit'
403 commit = parse_commit(''.join(it))
404 yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), False)
406 def ordered_tree_entries(tree_data, bupm=None):
407 """Yields (name, mangled_name, kind, gitmode, oid) for each item in
408 tree, sorted by name.
411 # Sadly, the .bupm entries currently aren't in git tree order,
412 # i.e. they don't account for the fact that git sorts trees
413 # (including our chunked trees) as if their names ended with "/",
414 # so "fo" sorts after "fo." iff fo is a directory. This makes
415 # streaming impossible when we need the metadata.
416 def result_from_tree_entry(tree_entry):
417 gitmode, mangled_name, oid = tree_entry
418 name, kind = git.demangle_name(mangled_name, gitmode)
419 return name, mangled_name, kind, gitmode, oid
421 tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
423 tree_ents = sorted(tree_ents, key=lambda x: x[0])
424 for ent in tree_ents:
427 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
429 def tree_item(ent_oid, kind, gitmode):
430 if kind == BUP_CHUNKED:
431 meta = Metadata.read(bupm) if bupm else default_file_mode
432 return Chunky(oid=ent_oid, meta=meta)
435 # No metadata here (accessable via '.' inside ent_oid).
436 return Item(meta=default_dir_mode, oid=ent_oid)
438 return Item(oid=ent_oid,
439 meta=(Metadata.read(bupm) if bupm \
440 else _default_mode_for_gitmode(gitmode)))
442 assert len(oid) == 20
444 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
445 yield '.', Item(oid=oid, meta=dot_meta)
446 tree_entries = ordered_tree_entries(tree_data, bupm)
447 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
448 if mangled_name == '.bupm':
451 yield name, tree_item(ent_oid, kind, gitmode)
454 # Assumes the tree is properly formed, i.e. there are no
455 # duplicates, and entries will be in git tree order.
456 if type(names) not in (frozenset, set):
457 names = frozenset(names)
458 remaining = len(names)
460 # Account for the bupm sort order issue (cf. ordered_tree_entries above)
461 last_name = max(names) if bupm else max(names) + '/'
464 dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
465 yield '.', Item(oid=oid, meta=dot_meta)
470 tree_entries = ordered_tree_entries(tree_data, bupm)
471 for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
472 if mangled_name == '.bupm':
475 if name not in names:
477 break # given bupm sort order, we're finished
478 if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
481 yield name, tree_item(ent_oid, kind, gitmode)
486 def tree_items_with_meta(repo, oid, tree_data, names):
487 # For now, the .bupm order doesn't quite match git's, and we don't
488 # load the tree data incrementally anyway, so we just work in RAM
490 assert len(oid) == 20
492 for _, mangled_name, sub_oid in tree_decode(tree_data):
493 if mangled_name == '.bupm':
494 bupm = _FileReader(repo, sub_oid)
496 if mangled_name > '.bupm':
498 for item in tree_items(oid, tree_data, names, bupm):
501 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
503 def _reverse_suffix_duplicates(strs):
504 """Yields the elements of strs, with any runs of duplicate values
505 suffixed with -N suffixes, where the zero padded integer N
506 decreases to 0 by 1 (e.g. 10, 09, ..., 00).
509 for name, duplicates in groupby(strs):
510 ndup = len(tuple(duplicates))
514 ndig = len(str(ndup - 1))
515 fmt = '%s-' + '%0' + str(ndig) + 'd'
516 for i in xrange(ndup - 1, -1, -1):
517 yield fmt % (name, i)
519 def _name_for_rev(rev):
520 commit, (tree_oidx, utc) = rev
521 assert len(commit) == 40
522 return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
524 def _item_for_rev(rev):
525 commit, (tree_oidx, utc) = rev
526 assert len(commit) == 40
527 assert len(tree_oidx) == 40
528 return Commit(meta=default_dir_mode,
529 oid=tree_oidx.decode('hex'),
530 coid=commit.decode('hex'))
532 def revlist_items(repo, oid, names):
533 assert len(oid) == 20
534 oidx = oid.encode('hex')
535 names = frozenset(name for name in (names or tuple()) \
536 if _save_name_rx.match(name) or name in ('.', 'latest'))
537 # Do this before we open the rev_list iterator so we're not nesting
538 if (not names) or ('.' in names):
539 yield '.', _revlist_item_from_oid(repo, oid, True)
541 revs = repo.rev_list((oidx,), format='%T %at', parse=parse_rev_auth_secs)
542 rev_items, rev_names = tee(revs)
543 revs = None # Don't disturb the tees
544 rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
545 rev_items = (_item_for_rev(x) for x in rev_items)
549 for item in rev_items:
550 first_commit = first_commit or item
551 yield next(rev_names), item
552 yield 'latest', first_commit
555 # Revs are in reverse chronological order by default
556 last_name = min(names)
557 for item in rev_items:
558 first_commit = first_commit or item
559 name = next(rev_names) # Might have -N dup suffix
562 if not name in names:
566 # FIXME: need real short circuit...
567 for _ in rev_items: pass
568 for _ in rev_names: pass
570 if 'latest' in names:
571 yield 'latest', first_commit
573 def tags_items(repo, names):
577 assert len(oid) == 20
578 oidx = oid.encode('hex')
580 _, typ, size = next(it)
582 return _commit_item_from_data(oid, ''.join(it))
585 return Item(meta=default_file_mode, oid=oid)
587 return Item(meta=default_dir_mode, oid=oid)
588 raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
592 # We have to pull these all into ram because tag_item calls cat()
593 for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
594 assert(name.startswith('refs/tags/'))
596 yield name, tag_item(oid)
599 # Assumes no duplicate refs
600 if type(names) not in (frozenset, set):
601 names = frozenset(names)
602 remaining = len(names)
603 last_name = max(names)
610 for name, oid in repo.refs(names, limit_to_tags=True):
611 assert(name.startswith('refs/tags/'))
615 if name not in names:
617 yield name, tag_item(oid)
622 def contents(repo, item, names=None, want_meta=True):
623 """Yields information about the items contained in item. Yields
624 (name, item) for each name in names, if the name exists, in an
625 unspecified order. If there are no names, then yields (name,
626 item) for all items, including, a first item named '.'
627 representing the container itself.
629 The meta value for any directories other than '.' will be a
630 default directory mode, not a Metadata object. This is because
631 the actual metadata for a directory is stored inside the directory
632 (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
634 Note that want_meta is advisory. For any given item, item.meta
635 might be a Metadata instance or a mode, and if the former,
636 meta.size might be None. Missing sizes can be computed via via
637 item_size() or augment_item_meta(..., include_size=True).
639 Do not modify any item.meta Metadata instances directly. If
640 needed, make a copy via item.meta.copy() and modify that instead.
643 # Q: are we comfortable promising '.' first when no names?
646 assert S_ISDIR(item_mode(item))
649 if item_t in real_tree_types:
650 it = repo.cat(item.oid.encode('hex'))
651 _, obj_type, size = next(it)
653 if obj_type == 'tree':
655 item_gen = tree_items_with_meta(repo, item.oid, data, names)
657 item_gen = tree_items(item.oid, data, names)
658 elif obj_type == 'commit':
660 item_gen = tree_items_with_meta(repo, item.oid, tree_data, names)
662 item_gen = tree_items(item.oid, tree_data, names)
665 raise Exception('unexpected git ' + obj_type)
666 elif item_t == RevList:
667 item_gen = revlist_items(repo, item.oid, names)
669 item_gen = root_items(repo, names)
671 item_gen = tags_items(repo, names)
673 raise Exception('unexpected VFS item ' + str(item))
677 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
678 def raise_dir_required_but_not_dir(path, parent, past):
679 raise IOError(ENOTDIR,
680 "path %r%s resolves to non-directory %r"
682 ' (relative to %r)' % parent if parent else '',
691 assert type(x[0]) in (bytes, str)
692 assert type(x[1]) in item_types
693 assert parent[0][1] == _root
694 if not S_ISDIR(item_mode(parent[-1][1])):
695 raise IOError(ENOTDIR,
696 'path resolution parent %r is not a directory'
698 is_absolute, must_be_dir, future = _decompose_path(path)
701 if not future: # path was effectively '.' or '/'
703 return (('', _root),)
710 past = list(parent) if parent else [('', _root)]
714 if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
715 raise_dir_required_but_not_dir(path, parent, past)
717 segment = future.pop()
720 if len(past) > 1: # .. from / is /
721 assert S_ISDIR(item_mode(past[-1][1]))
724 parent_name, parent_item = past[-1]
725 wanted = (segment,) if not want_meta else ('.', segment)
726 items = tuple(contents(repo, parent_item, names=wanted,
727 want_meta=want_meta))
729 item = items[0][1] if items else None
730 else: # First item will be '.' and have the metadata
731 item = items[1][1] if len(items) == 2 else None
732 dot, dot_item = items[0]
734 past[-1] = parent_name, parent_item
736 past.append((segment, None),)
738 mode = item_mode(item)
739 if not S_ISLNK(mode):
740 if not S_ISDIR(mode):
741 past.append((segment, item),)
743 raise IOError(ENOTDIR,
744 'path %r%s ends internally in non-directory here: %r'
746 ' (relative to %r)' % parent if parent else '',
750 raise_dir_required_but_not_dir(path, parent, past)
753 if want_meta and type(item) in real_tree_types:
754 dir_meta = _find_treeish_oid_metadata(repo, item.oid)
756 item = item._replace(meta=dir_meta)
757 past.append((segment, item))
759 if not future and not deref:
760 past.append((segment, item),)
764 'too many symlinks encountered while resolving %r%s'
765 % (path, ' relative to %r' % parent if parent else ''),
766 terminus=tuple(past + [(segment, item)]))
767 target = readlink(repo, item)
768 is_absolute, _, target_future = _decompose_path(target)
770 if not target_future: # path was effectively '/'
771 return (('', _root),)
773 future = target_future
775 future.extend(target_future)
778 def lresolve(repo, path, parent=None, want_meta=True):
779 """Perform exactly the same function as resolve(), except if the final
780 path element is a symbolic link, don't follow it, just return it
784 return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
787 def resolve(repo, path, parent=None, want_meta=True):
788 """Follow the path in the virtual filesystem and return a tuple
789 representing the location, if any, denoted by the path. Each
790 element in the result tuple will be (name, info), where info will
791 be a VFS item that can be passed to functions like item_mode().
793 If a path segment that does not exist is encountered during
794 resolution, the result will represent the location of the missing
795 item, and that item in the result will be None.
797 Any attempt to traverse a non-directory will raise a VFS ENOTDIR
800 Any symlinks along the path, including at the end, will be
801 resolved. A VFS IOError with the errno attribute set to ELOOP
802 will be raised if too many symlinks are traversed while following
803 the path. That exception is effectively like a normal
804 ELOOP IOError exception, but will include a terminus element
805 describing the location of the failure, which will be a tuple of
806 (name, info) elements.
808 The parent, if specified, must be a sequence of (name, item)
809 tuples, and will provide the starting point for the resolution of
810 the path. If no parent is specified, resolution will start at
813 The result may include elements of parent directly, so they must
814 not be modified later. If this is a concern, pass in "name,
815 copy_item(item) for name, item in parent" instead.
817 When want_meta is true, detailed metadata will be included in each
818 result item if it's avaiable, otherwise item.meta will be an
819 integer mode. The metadata size may or may not be provided, but
820 can be computed by item_size() or augment_item_meta(...,
821 include_size=True). Setting want_meta=False is rarely desirable
822 since it can limit the VFS to just the metadata git itself can
823 represent, and so, as an example, fifos and sockets will appear to
824 be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
825 But the option is provided because it may be more efficient when
826 only the path names or the more limited metadata is sufficient.
828 Do not modify any item.meta Metadata instances directly. If
829 needed, make a copy via item.meta.copy() and modify that instead.
832 result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
834 _, leaf_item = result[-1]
836 assert not S_ISLNK(item_mode(leaf_item))
839 def augment_item_meta(repo, item, include_size=False):
840 """Ensure item has a Metadata instance for item.meta. If item.meta is
841 currently a mode, replace it with a compatible "fake" Metadata
842 instance. If include_size is true, ensure item.meta.size is
843 correct, computing it if needed. If item.meta is a Metadata
844 instance, this call may modify it in place or replace it.
847 # If we actually had parallelism, we'd need locking...
850 if isinstance(m, Metadata):
851 if include_size and m.size is None:
852 m.size = _compute_item_size(repo, item)
853 return item._replace(meta=m)
858 meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
860 target = _readlink(repo, item.oid)
861 meta.symlink_target = target
862 meta.size = len(target)
864 meta.size = _compute_item_size(repo, item)
865 return item._replace(meta=meta)
867 def fill_in_metadata_if_dir(repo, item):
868 """If item is a directory and item.meta is not a Metadata instance,
869 attempt to find the metadata for the directory. If found, return
870 a new item augmented to include that metadata. Otherwise, return
871 item. May be useful for the output of contents().
874 if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
875 items = tuple(contents(repo, item, ('.',), want_meta=True))
876 assert len(items) == 1
877 assert items[0][0] == '.'
881 def ensure_item_has_metadata(repo, item, include_size=False):
882 """If item is a directory, attempt to find and add its metadata. If
883 the item still doesn't have a Metadata instance for item.meta,
884 give it one via augment_item_meta(). May be useful for the output
888 return augment_item_meta(repo,
889 fill_in_metadata_if_dir(repo, item),
890 include_size=include_size)