]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
b84e4c886f25a1dcf50e88cb417c286cd3806b36
[bup.git] / lib / bup / vfs.py
1 """Virtual File System interface to bup repository content.
2
3 This module provides a path-based interface to the content of a bup
4 repository.
5
6 The VFS is structured like this:
7
8   /SAVE-NAME/latest/...
9   /SAVE-NAME/SAVE-DATE/...
10   /.tag/TAG-NAME/...
11
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
17 copy_item().
18
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().
23
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.
30
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(...,
33 include_size=True).
34
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()).
40
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
45 item.coid.
46
47 """
48
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
57
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
64
65
66 class IOError(exceptions.IOError):
67     def __init__(self, errno, message, terminus=None):
68         exceptions.IOError.__init__(self, errno, message)
69         self.terminus = terminus
70
71 default_file_mode = S_IFREG | 0o644
72 default_dir_mode = S_IFDIR | 0o755
73 default_symlink_mode = S_IFLNK | 0o755
74
75 def _default_mode_for_gitmode(gitmode):
76     if S_ISREG(gitmode):
77         return default_file_mode
78     if S_ISDIR(gitmode):
79         return default_dir_mode
80     if S_ISLNK(gitmode):
81         return default_symlink_mode
82     raise Exception('unexpected git mode ' + oct(gitmode))
83
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)
89     ofs = 0
90     while obj_t == 'tree':
91         mode, name, last_oid = last(tree_decode(''.join(it)))
92         ofs += int(name, 16)
93         it = repo.cat(last_oid.encode('hex'))
94         _, obj_t, size = next(it)
95     return ofs + sum(len(b) for b in it)
96
97 def _tree_chunks(repo, tree, startofs):
98     "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
99     assert(startofs >= 0)
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:
103         ofs = int(name, 16)
104         skipmore = startofs - ofs
105         if skipmore < 0:
106             skipmore = 0
107         it = repo.cat(oid.encode('hex'))
108         _, obj_t, size = next(it)
109         data = ''.join(it)            
110         if S_ISDIR(mode):
111             assert obj_t == 'tree'
112             for b in _tree_chunks(repo, tree_decode(data), skipmore):
113                 yield b
114         else:
115             assert obj_t == 'blob'
116             yield data[skipmore:]
117
118 class _ChunkReader:
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'
123         data = ''.join(it)
124         if isdir:
125             self.it = _tree_chunks(repo, tree_decode(data), startofs)
126             self.blob = None
127         else:
128             self.it = None
129             self.blob = data[startofs:]
130         self.ofs = startofs
131
132     def next(self, size):
133         out = ''
134         while len(out) < size:
135             if self.it and not self.blob:
136                 try:
137                     self.blob = self.it.next()
138                 except StopIteration:
139                     self.it = None
140             if self.blob:
141                 want = size - len(out)
142                 out += self.blob[:want]
143                 self.blob = self.blob[want:]
144             if not self.it:
145                 break
146         debug2('next(%d) returned %d\n' % (size, len(out)))
147         self.ofs += len(out)
148         return out
149
150 class _FileReader(object):
151     def __init__(self, repo, oid, known_size=None):
152         assert len(oid) == 20
153         self.oid = oid
154         self.ofs = 0
155         self.reader = None
156         self._repo = repo
157         self._size = known_size
158
159     def _compute_size(self):
160         if not self._size:
161             self._size = _normal_or_chunked_file_size(self._repo, self.oid)
162         return self._size
163         
164     def seek(self, ofs):
165         if ofs < 0 or ofs > self._compute_size():
166             raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
167         self.ofs = ofs
168
169     def tell(self):
170         return self.ofs
171
172     def read(self, count=-1):
173         size = self._compute_size()
174         if self.ofs >= size:
175             return ''
176         if count < 0:
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)
180         try:
181             buf = self.reader.next(count)
182         except:
183             self.reader = None
184             raise  # our offsets will be all screwed up otherwise
185         self.ofs += len(buf)
186         return buf
187
188     def close(self):
189         pass
190
191     def __enter__(self):
192         return self
193     def __exit__(self, type, value, traceback):
194         self.close()
195         return False
196
197 _multiple_slashes_rx = re.compile(r'//+')
198
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.
204
205     """
206     path = re.sub(_multiple_slashes_rx, '/', path)
207     if path == '/':
208         return True, True, []
209     is_absolute = must_be_dir = False
210     if path.startswith('/'):
211         is_absolute = True
212         path = path[1:]
213     for suffix in ('/', '/.'):
214         if path.endswith(suffix):
215             must_be_dir = True
216             path = path[:-len(suffix)]
217     parts = [x for x in path.split('/') if x != '.']
218     parts.reverse()
219     if not parts:
220         must_be_dir = True  # e.g. path was effectively '.' or '/', etc.
221     return is_absolute, must_be_dir, parts
222     
223
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'))
230
231 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
232 real_tree_types = frozenset((Item, Commit))
233
234 _root = Root(meta=default_dir_mode)
235 _tags = Tags(meta=default_dir_mode)
236
237
238 ### vfs cache
239
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.
244
245 _cache = {}
246 _cache_keys = []
247 _cache_max_items = 30000
248
249 def clear_cache():
250     global _cache, _cache_keys
251     _cache = {}
252     _cache_keys = []
253
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
258       commit_oid -> commit
259       commit_oid + ':r' -> rev-list
260          i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
261     """
262     # Suspect we may eventually add "(container_oid, name) -> ...", and others.
263     x_t = type(x)
264     if x_t is bytes:
265         if len(x) == 20:
266             return True
267         if len(x) == 22 and x.endswith(b':r'):
268             return True
269         if x.startswith('res:'):
270             return True
271
272 def cache_get(key):
273     global _cache
274     assert is_valid_cache_key(key)
275     return _cache.get(key)
276
277 def cache_notice(key, value):
278     global _cache, _cache_keys, _cache_max_items
279     assert is_valid_cache_key(key)
280     if key in _cache:
281         return
282     if len(_cache) < _cache_max_items:
283         _cache_keys.append(key)
284         _cache[key] = value
285         return
286     victim_i = randrange(0, len(_cache_keys))
287     victim = _cache_keys[victim_i]
288     del _cache[victim]
289     _cache_keys[victim_i] = key
290     _cache[key] = value
291
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
295     mode."""
296     # tree might be stored independently, or as '.' with its entries.
297     item = cache_get(oid)
298     if item:
299         if not need_meta:
300             return item
301         if isinstance(item.meta, Metadata):
302             return item
303     entries = cache_get(oid + b':r')
304     if entries:
305         return entries['.']
306
307 def cache_get_revlist_item(oid, need_meta=True):
308     commit = cache_get_commit_item(oid, need_meta=need_meta)
309     if commit:
310         return RevList(oid=oid, meta=commit.meta)
311
312 def copy_item(item):
313     """Return a completely independent copy of item, such that
314     modifications will not affect the original.
315
316     """
317     meta = getattr(item, 'meta', None)
318     if isinstance(meta, Metadata):
319         return(item._replace(meta=meta.copy()))
320     return item
321
322 def item_mode(item):
323     """Return the integer mode (stat st_mode) for item."""
324     m = item.meta
325     if isinstance(m, Metadata):
326         return m.mode
327     return m
328
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)
333     if not m:
334         return default_dir_mode
335     assert m.mode is not None
336     if m.size is None:
337         m.size = 0
338     return m
339
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).
343
344     """    
345     assert len(oid) == 20
346     it = repo.cat(oid.encode('hex'))
347     _, item_t, size = next(it)
348     data = ''.join(it)
349     if item_t == 'commit':
350         commit = parse_commit(data)
351         it = repo.cat(commit.tree)
352         _, item_t, size = next(it)
353         data = ''.join(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':
359             return data, sub_oid
360         if mangled_name > '.bupm':
361             break
362     return data, None
363
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).
367
368     """
369     tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
370     if bupm_oid:
371         with _FileReader(repo, bupm_oid) as meta_stream:
372             return _read_dir_meta(meta_stream)
373     return None
374
375 def _readlink(repo, oid):
376     return ''.join(repo.join(oid.encode('hex')))
377
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."""
381     assert repo
382     assert S_ISLNK(item_mode(item))
383     if isinstance(item.meta, Metadata):
384         target = item.meta.symlink_target
385         if target:
386             return target
387     return _readlink(repo, item.oid)
388
389 def _compute_item_size(repo, item):
390     mode = item_mode(item)
391     if S_ISREG(mode):
392         size = _normal_or_chunked_file_size(repo, item.oid)
393         return size
394     if S_ISLNK(mode):
395         return len(_readlink(repo, item.oid))
396     return 0
397
398 def item_size(repo, item):
399     """Return the size of item, computing it if necessary."""
400     m = item.meta
401     if isinstance(m, Metadata) and m.size is not None:
402         return m.size
403     return _compute_item_size(repo, item)
404
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)
409
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)
414
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'),
419                   coid=oid)
420
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)):
424         return commit
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))
429     if require_meta:
430         meta = _find_treeish_oid_metadata(repo, commit.oid)
431         if meta:
432             commit = commit._replace(meta=meta)
433     cache_notice(oid, commit)
434     return commit
435
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)
439
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.
444
445     """
446     # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
447
448     global _root, _tags
449     if not names:
450         yield '.', _root
451         yield '.tag', _tags
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)
458         return
459
460     if '.' in names:
461         yield '.', _root
462     if '.tag' in names:
463         yield '.tag', _tags
464     for ref in names:
465         if ref in ('.', '.tag'):
466             continue
467         it = repo.cat('refs/heads/' + ref)
468         oidx, typ, size = next(it)
469         if not oidx:
470             for _ in it: pass
471             continue
472         assert typ == 'commit'
473         commit = parse_commit(''.join(it))
474         yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
475
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.
479
480     """
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
490
491     tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
492     if bupm:
493         tree_ents = sorted(tree_ents, key=lambda x: x[0])
494     for ent in tree_ents:
495         yield ent
496     
497 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
498
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)
503
504         if S_ISDIR(gitmode):
505             # No metadata here (accessable via '.' inside ent_oid).
506             return Item(meta=default_dir_mode, oid=ent_oid)
507
508         return Item(oid=ent_oid,
509                     meta=(Metadata.read(bupm) if bupm \
510                           else _default_mode_for_gitmode(gitmode)))
511
512     assert len(oid) == 20
513     if not names:
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':
519                 continue
520             assert name != '.'
521             yield name, tree_item(ent_oid, kind, gitmode)
522         return
523
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)
529
530     # Account for the bupm sort order issue (cf. ordered_tree_entries above)
531     last_name = max(names) if bupm else max(names) + '/'
532
533     if '.' in names:
534         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
535         yield '.', Item(oid=oid, meta=dot_meta)
536         if remaining == 1:
537             return
538         remaining -= 1
539
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':
543             continue
544         assert name != '.'
545         if name not in names:
546             if name > last_name:
547                 break  # given bupm sort order, we're finished
548             if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
549                 Metadata.read(bupm)
550             continue
551         yield name, tree_item(ent_oid, kind, gitmode)
552         if remaining == 1:
553             break
554         remaining -= 1
555
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
559     # via tree_data.
560     assert len(oid) == 20
561     bupm = None
562     for _, mangled_name, sub_oid in tree_decode(tree_data):
563         if mangled_name == '.bupm':
564             bupm = _FileReader(repo, sub_oid)
565             break
566         if mangled_name > '.bupm':
567             break
568     for item in tree_items(oid, tree_data, names, bupm):
569         yield item
570
571 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
572         
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).
577
578     """
579     for name, duplicates in groupby(strs):
580         ndup = len(tuple(duplicates))
581         if ndup == 1:
582             yield name
583         else:
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)
588
589 def parse_rev(f):
590     items = f.readline().split(None)
591     assert len(items) == 2
592     tree, auth_sec = items
593     return tree.decode('hex'), int(auth_sec)
594
595 def _name_for_rev(rev):
596     commit_oidx, (tree_oid, utc) = rev
597     return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
598
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)
603     if item:
604         return item
605     item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
606     cache_notice(item.coid, item)
607     return item
608
609 def cache_commit(repo, oid):
610     """Build, cache, and return a "name -> commit_item" dict of the entire
611     commit rev-list.
612
613     """
614     # For now, always cache with full metadata
615     entries = {}
616     entries['.'] = _revlist_item_from_oid(repo, oid, True)
617     revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
618                          parse=parse_rev)
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)
623     latest = None
624     for item in rev_items:
625         latest = latest or item
626         name = next(rev_names)
627         entries[name] = item
628     entries['latest'] = latest
629     cache_notice(latest.coid + b':r', entries)
630     return entries
631
632 def revlist_items(repo, oid, names):
633     assert len(oid) == 20
634
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)
639         return
640
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')
644     if not entries:
645         entries = cache_commit(repo, oid)
646
647     if not names:
648         for name in sorted(entries.keys()):
649             yield name, entries[name]
650         return
651
652     names = frozenset(name for name in names
653                       if _save_name_rx.match(name) or name in ('.', 'latest'))
654
655     if '.' in names:
656         yield '.', entries['.']
657     for name in (n for n in names if n != '.'):
658         commit = entries.get(name)
659         if commit:
660             yield name, commit
661
662 def tags_items(repo, names):
663     global _tags
664
665     def tag_item(oid):
666         assert len(oid) == 20
667         oidx = oid.encode('hex')
668         it = repo.cat(oidx)
669         _, typ, size = next(it)
670         if typ == 'commit':
671             return cache_get_commit_item(oid, need_meta=False) \
672                 or _commit_item_from_data(oid, ''.join(it))
673         for _ in it: pass
674         if typ == 'blob':
675             return Item(meta=default_file_mode, oid=oid)
676         elif typ == 'tree':
677             return Item(meta=default_dir_mode, oid=oid)
678         raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
679
680     if not names:
681         yield '.', _tags
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/'))
685             name = name[10:]
686             yield name, tag_item(oid)
687         return
688
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)
694     if '.' in names:
695         yield '.', _tags
696         if remaining == 1:
697             return
698         remaining -= 1
699
700     for name, oid in repo.refs(names, limit_to_tags=True):
701         assert(name.startswith('refs/tags/'))
702         name = name[10:]
703         if name > last_name:
704             return
705         if name not in names:
706             continue
707         yield name, tag_item(oid)
708         if remaining == 1:
709             return
710         remaining -= 1
711
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.
718
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()).
723
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).
728
729     Do not modify any item.meta Metadata instances directly.  If
730     needed, make a copy via item.meta.copy() and modify that instead.
731
732     """
733     # Q: are we comfortable promising '.' first when no names?
734     global _root, _tags
735     assert repo
736     assert S_ISDIR(item_mode(item))
737     item_t = type(item)
738     if item_t in real_tree_types:
739         it = repo.cat(item.oid.encode('hex'))
740         _, obj_t, size = next(it)
741         data = ''.join(it)
742         if obj_t != 'tree':
743             for _ in it: pass
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)
747         if want_meta:
748             item_gen = tree_items_with_meta(repo, item.oid, data, names)
749         else:
750             item_gen = tree_items(item.oid, data, names)
751     elif item_t == RevList:
752         item_gen = revlist_items(repo, item.oid, names)
753     elif item_t == Root:
754         item_gen = root_items(repo, names, want_meta)
755     elif item_t == Tags:
756         item_gen = tags_items(repo, names)
757     else:
758         raise Exception('unexpected VFS item ' + str(item))
759     for x in item_gen:
760         yield x
761
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 ''),
766                    '/'.join(path))
767     resolution = cache_get(cache_key)
768     if resolution:
769         return resolution
770
771     def notice_resolution(r):
772         cache_notice(cache_key, r)
773         return r
774
775     def raise_dir_required_but_not_dir(path, parent, past):
776         raise IOError(ENOTDIR,
777                       "path %r%s resolves to non-directory %r"
778                       % (path,
779                          ' (relative to %r)' % parent if parent else '',
780                          past),
781                       terminus=past)
782     global _root
783     assert repo
784     assert len(path)
785     if parent:
786         for x in parent:
787             assert len(x) == 2
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'
794                           % (parent,))
795     is_absolute, must_be_dir, future = _decompose_path(path)
796     if must_be_dir:
797         deref = True
798     if not future:  # path was effectively '.' or '/'
799         if is_absolute:
800             return notice_resolution((('', _root),))
801         if parent:
802             return notice_resolution(tuple(parent))
803         return notice_resolution((('', _root),))
804     if is_absolute:
805         past = [('', _root)]
806     else:
807         past = list(parent) if parent else [('', _root)]
808     hops = 0
809     while True:
810         if not future:
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()
815         if segment == '..':
816             assert len(past) > 0
817             if len(past) > 1:  # .. from / is /
818                 assert S_ISDIR(item_mode(past[-1][1]))
819                 past.pop()
820         else:
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))
825             if not 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]
830                 assert dot == '.'
831                 past[-1] = parent_name, parent_item
832             if not 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),)
839                     if future:
840                         raise IOError(ENOTDIR,
841                                       'path %r%s ends internally in non-directory here: %r'
842                                       % (path,
843                                          ' (relative to %r)' % parent if parent else '',
844                                          past),
845                                       terminus=past)
846                     if must_be_dir:
847                         raise_dir_required_but_not_dir(path, parent, past)
848                     return notice_resolution(tuple(past))
849                 # It's treeish
850                 if want_meta and type(item) in real_tree_types:
851                     dir_meta = _find_treeish_oid_metadata(repo, item.oid)
852                     if dir_meta:
853                         item = item._replace(meta=dir_meta)
854                 past.append((segment, item))
855             else:  # symlink
856                 if not future and not deref:
857                     past.append((segment, item),)
858                     continue
859                 if hops > 100:
860                     raise IOError(ELOOP,
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)
866                 if is_absolute:
867                     if not target_future:  # path was effectively '/'
868                         return notice_resolution((('', _root),))
869                     past = [('', _root)]
870                     future = target_future
871                 else:
872                     future.extend(target_future)
873                 hops += 1
874                 
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
878     in the result.
879
880     """
881     return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
882                          deref=False)
883
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().
889
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.
893
894     Any attempt to traverse a non-directory will raise a VFS ENOTDIR
895     IOError exception.
896
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.
904
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
908     '/'.
909
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.
913
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.
924
925     Do not modify any item.meta Metadata instances directly.  If
926     needed, make a copy via item.meta.copy() and modify that instead.
927
928     """
929     result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
930                            deref=True)
931     _, leaf_item = result[-1]
932     if leaf_item:
933         assert not S_ISLNK(item_mode(leaf_item))
934     return result
935
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.
940
941     """
942     res = lresolve(repo, path, parent=parent, want_meta=want_meta)
943     leaf_name, leaf_item = res[-1]
944     if not leaf_item:
945         return res
946     if not S_ISLNK(item_mode(leaf_item)):
947         return res
948     deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
949     deref_name, deref_item = deref[-1]
950     if deref_item:
951         return deref
952     return res
953
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.
960
961     """
962     # If we actually had parallelism, we'd need locking...
963     assert repo
964     m = item.meta
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)
969         return item
970     # m is mode
971     meta = Metadata()
972     meta.mode = m
973     meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
974     if S_ISLNK(m):
975         target = _readlink(repo, item.oid)
976         meta.symlink_target = target
977         meta.size = len(target)
978     elif include_size:
979         meta.size = _compute_item_size(repo, item)
980     return item._replace(meta=meta)
981
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().
987
988     """
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] == '.'
993         item = items[0][1]
994     return item
995
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
1000     of contents().
1001
1002     """
1003     return augment_item_meta(repo,
1004                              fill_in_metadata_if_dir(repo, item),
1005                              include_size=include_size)