]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
99ca85d39426a0de4e35617625872d9b804f9491
[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 _skip_chunks_before_offset(tree, offset):
98     prev_ent = next(tree, None)
99     if not prev_ent:
100         return tree
101     ent = None
102     for ent in tree:
103         ent_ofs = int(ent[1], 16)
104         if ent_ofs > offset:
105             return chain([prev_ent, ent], tree)
106         if ent_ofs == offset:
107             return chain([ent], tree)
108         prev_ent = ent
109     return [prev_ent]
110
111 def _tree_chunks(repo, tree, startofs):
112     "Tree should be a sequence of (name, mode, hash) as per tree_decode()."
113     assert(startofs >= 0)
114     # name is the chunk's hex offset in the original file
115     for mode, name, oid in _skip_chunks_before_offset(tree, startofs):
116         ofs = int(name, 16)
117         skipmore = startofs - ofs
118         if skipmore < 0:
119             skipmore = 0
120         it = repo.cat(oid.encode('hex'))
121         _, obj_t, size = next(it)
122         data = ''.join(it)            
123         if S_ISDIR(mode):
124             assert obj_t == 'tree'
125             for b in _tree_chunks(repo, tree_decode(data), skipmore):
126                 yield b
127         else:
128             assert obj_t == 'blob'
129             yield data[skipmore:]
130
131 class _ChunkReader:
132     def __init__(self, repo, oid, startofs):
133         it = repo.cat(oid.encode('hex'))
134         _, obj_t, size = next(it)
135         isdir = obj_t == 'tree'
136         data = ''.join(it)
137         if isdir:
138             self.it = _tree_chunks(repo, tree_decode(data), startofs)
139             self.blob = None
140         else:
141             self.it = None
142             self.blob = data[startofs:]
143         self.ofs = startofs
144
145     def next(self, size):
146         out = ''
147         while len(out) < size:
148             if self.it and not self.blob:
149                 try:
150                     self.blob = self.it.next()
151                 except StopIteration:
152                     self.it = None
153             if self.blob:
154                 want = size - len(out)
155                 out += self.blob[:want]
156                 self.blob = self.blob[want:]
157             if not self.it:
158                 break
159         debug2('next(%d) returned %d\n' % (size, len(out)))
160         self.ofs += len(out)
161         return out
162
163 class _FileReader(object):
164     def __init__(self, repo, oid, known_size=None):
165         assert len(oid) == 20
166         self.oid = oid
167         self.ofs = 0
168         self.reader = None
169         self._repo = repo
170         self._size = known_size
171
172     def _compute_size(self):
173         if not self._size:
174             self._size = _normal_or_chunked_file_size(self._repo, self.oid)
175         return self._size
176         
177     def seek(self, ofs):
178         if ofs < 0 or ofs > self._compute_size():
179             raise IOError(EINVAL, 'Invalid seek offset: %d' % ofs)
180         self.ofs = ofs
181
182     def tell(self):
183         return self.ofs
184
185     def read(self, count=-1):
186         size = self._compute_size()
187         if self.ofs >= size:
188             return ''
189         if count < 0:
190             count = size - self.ofs
191         if not self.reader or self.reader.ofs != self.ofs:
192             self.reader = _ChunkReader(self._repo, self.oid, self.ofs)
193         try:
194             buf = self.reader.next(count)
195         except:
196             self.reader = None
197             raise  # our offsets will be all screwed up otherwise
198         self.ofs += len(buf)
199         return buf
200
201     def close(self):
202         pass
203
204     def __enter__(self):
205         return self
206     def __exit__(self, type, value, traceback):
207         self.close()
208         return False
209
210 _multiple_slashes_rx = re.compile(r'//+')
211
212 def _decompose_path(path):
213     """Return a boolean indicating whether the path is absolute, and a
214     reversed list of path elements, omitting any occurrences of "."
215     and ignoring any leading or trailing slash.  If the path is
216     effectively '/' or '.', return an empty list.
217
218     """
219     path = re.sub(_multiple_slashes_rx, '/', path)
220     if path == '/':
221         return True, True, []
222     is_absolute = must_be_dir = False
223     if path.startswith('/'):
224         is_absolute = True
225         path = path[1:]
226     for suffix in ('/', '/.'):
227         if path.endswith(suffix):
228             must_be_dir = True
229             path = path[:-len(suffix)]
230     parts = [x for x in path.split('/') if x != '.']
231     parts.reverse()
232     if not parts:
233         must_be_dir = True  # e.g. path was effectively '.' or '/', etc.
234     return is_absolute, must_be_dir, parts
235     
236
237 Item = namedtuple('Item', ('meta', 'oid'))
238 Chunky = namedtuple('Chunky', ('meta', 'oid'))
239 Root = namedtuple('Root', ('meta'))
240 Tags = namedtuple('Tags', ('meta'))
241 RevList = namedtuple('RevList', ('meta', 'oid'))
242 Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
243
244 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
245 real_tree_types = frozenset((Item, Commit))
246
247 _root = Root(meta=default_dir_mode)
248 _tags = Tags(meta=default_dir_mode)
249
250
251 ### vfs cache
252
253 ### A general purpose shared cache with (currently) cheap random
254 ### eviction.  At the moment there is no weighting so a single commit
255 ### item is just as likely to be evicted as an entire "rev-list".  See
256 ### is_valid_cache_key for a description of the expected content.
257
258 _cache = {}
259 _cache_keys = []
260 _cache_max_items = 30000
261
262 def clear_cache():
263     global _cache, _cache_keys
264     _cache = {}
265     _cache_keys = []
266
267 def is_valid_cache_key(x):
268     """Return logically true if x looks like it could be a valid cache key
269     (with respect to structure).  Current valid cache entries:
270       res:... -> resolution
271       commit_oid -> commit
272       commit_oid + ':r' -> rev-list
273          i.e. rev-list -> {'.', commit, '2012...', next_commit, ...}
274     """
275     # Suspect we may eventually add "(container_oid, name) -> ...", and others.
276     x_t = type(x)
277     if x_t is bytes:
278         if len(x) == 20:
279             return True
280         if len(x) == 22 and x.endswith(b':r'):
281             return True
282         if x.startswith('res:'):
283             return True
284
285 def cache_get(key):
286     global _cache
287     assert is_valid_cache_key(key)
288     return _cache.get(key)
289
290 def cache_notice(key, value):
291     global _cache, _cache_keys, _cache_max_items
292     assert is_valid_cache_key(key)
293     if key in _cache:
294         return
295     if len(_cache) < _cache_max_items:
296         _cache_keys.append(key)
297         _cache[key] = value
298         return
299     victim_i = randrange(0, len(_cache_keys))
300     victim = _cache_keys[victim_i]
301     del _cache[victim]
302     _cache_keys[victim_i] = key
303     _cache[key] = value
304
305 def cache_get_commit_item(oid, need_meta=True):
306     """Return the requested tree item if it can be found in the cache.
307     When need_meta is true don't return a cached item that only has a
308     mode."""
309     # tree might be stored independently, or as '.' with its entries.
310     item = cache_get(oid)
311     if item:
312         if not need_meta:
313             return item
314         if isinstance(item.meta, Metadata):
315             return item
316     entries = cache_get(oid + b':r')
317     if entries:
318         return entries['.']
319
320 def cache_get_revlist_item(oid, need_meta=True):
321     commit = cache_get_commit_item(oid, need_meta=need_meta)
322     if commit:
323         return RevList(oid=oid, meta=commit.meta)
324
325 def copy_item(item):
326     """Return a completely independent copy of item, such that
327     modifications will not affect the original.
328
329     """
330     meta = getattr(item, 'meta', None)
331     if isinstance(meta, Metadata):
332         return(item._replace(meta=meta.copy()))
333     return item
334
335 def item_mode(item):
336     """Return the integer mode (stat st_mode) for item."""
337     m = item.meta
338     if isinstance(m, Metadata):
339         return m.mode
340     return m
341
342 def _read_dir_meta(bupm):
343     # This is because save writes unmodified Metadata() entries for
344     # fake parents -- test-save-strip-graft.sh demonstrates.
345     m = Metadata.read(bupm)
346     if not m:
347         return default_dir_mode
348     assert m.mode is not None
349     if m.size is None:
350         m.size = 0
351     return m
352
353 def tree_data_and_bupm(repo, oid):
354     """Return (tree_bytes, bupm_oid) where bupm_oid will be None if the
355     tree has no metadata (i.e. older bup save, or non-bup tree).
356
357     """    
358     assert len(oid) == 20
359     it = repo.cat(oid.encode('hex'))
360     _, item_t, size = next(it)
361     data = ''.join(it)
362     if item_t == 'commit':
363         commit = parse_commit(data)
364         it = repo.cat(commit.tree)
365         _, item_t, size = next(it)
366         data = ''.join(it)
367         assert item_t == 'tree'
368     elif item_t != 'tree':
369         raise Exception('%r is not a tree or commit' % oid.encode('hex'))
370     for _, mangled_name, sub_oid in tree_decode(data):
371         if mangled_name == '.bupm':
372             return data, sub_oid
373         if mangled_name > '.bupm':
374             break
375     return data, None
376
377 def _find_treeish_oid_metadata(repo, oid):
378     """Return the metadata for the tree or commit oid, or None if the tree
379     has no metadata (i.e. older bup save, or non-bup tree).
380
381     """
382     tree_data, bupm_oid = tree_data_and_bupm(repo, oid)
383     if bupm_oid:
384         with _FileReader(repo, bupm_oid) as meta_stream:
385             return _read_dir_meta(meta_stream)
386     return None
387
388 def _readlink(repo, oid):
389     return ''.join(repo.join(oid.encode('hex')))
390
391 def readlink(repo, item):
392     """Return the link target of item, which must be a symlink.  Reads the
393     target from the repository if necessary."""
394     assert repo
395     assert S_ISLNK(item_mode(item))
396     if isinstance(item.meta, Metadata):
397         target = item.meta.symlink_target
398         if target:
399             return target
400     return _readlink(repo, item.oid)
401
402 def _compute_item_size(repo, item):
403     mode = item_mode(item)
404     if S_ISREG(mode):
405         size = _normal_or_chunked_file_size(repo, item.oid)
406         return size
407     if S_ISLNK(mode):
408         return len(_readlink(repo, item.oid))
409     return 0
410
411 def item_size(repo, item):
412     """Return the size of item, computing it if necessary."""
413     m = item.meta
414     if isinstance(m, Metadata) and m.size is not None:
415         return m.size
416     return _compute_item_size(repo, item)
417
418 def tree_data_reader(repo, oid):
419     """Return an open reader for all of the data contained within oid.  If
420     oid refers to a tree, recursively concatenate all of its contents."""
421     return _FileReader(repo, oid)
422
423 def fopen(repo, item):
424     """Return an open reader for the given file item."""
425     assert S_ISREG(item_mode(item))
426     return tree_data_reader(repo, item.oid)
427
428 def _commit_item_from_data(oid, data):
429     info = parse_commit(data)
430     return Commit(meta=default_dir_mode,
431                   oid=info.tree.decode('hex'),
432                   coid=oid)
433
434 def _commit_item_from_oid(repo, oid, require_meta):
435     commit = cache_get_commit_item(oid, need_meta=require_meta)
436     if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
437         return commit
438     it = repo.cat(oid.encode('hex'))
439     _, typ, size = next(it)
440     assert typ == 'commit'
441     commit = _commit_item_from_data(oid, ''.join(it))
442     if require_meta:
443         meta = _find_treeish_oid_metadata(repo, commit.oid)
444         if meta:
445             commit = commit._replace(meta=meta)
446     cache_notice(oid, commit)
447     return commit
448
449 def _revlist_item_from_oid(repo, oid, require_meta):
450     commit = _commit_item_from_oid(repo, oid, require_meta)
451     return RevList(oid=oid, meta=commit.meta)
452
453 def root_items(repo, names=None, want_meta=True):
454     """Yield (name, item) for the items in '/' in the VFS.  Return
455     everything if names is logically false, otherwise return only
456     items with a name in the collection.
457
458     """
459     # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
460
461     global _root, _tags
462     if not names:
463         yield '.', _root
464         yield '.tag', _tags
465         # FIXME: maybe eventually support repo.clone() or something
466         # and pass in two repos, so we can drop the tuple() and stream
467         # in parallel (i.e. meta vs refs).
468         for name, oid in tuple(repo.refs([], limit_to_heads=True)):
469             assert(name.startswith('refs/heads/'))
470             yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
471         return
472
473     if '.' in names:
474         yield '.', _root
475     if '.tag' in names:
476         yield '.tag', _tags
477     for ref in names:
478         if ref in ('.', '.tag'):
479             continue
480         it = repo.cat('refs/heads/' + ref)
481         oidx, typ, size = next(it)
482         if not oidx:
483             for _ in it: pass
484             continue
485         assert typ == 'commit'
486         commit = parse_commit(''.join(it))
487         yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
488
489 def ordered_tree_entries(tree_data, bupm=None):
490     """Yields (name, mangled_name, kind, gitmode, oid) for each item in
491     tree, sorted by name.
492
493     """
494     # Sadly, the .bupm entries currently aren't in git tree order,
495     # i.e. they don't account for the fact that git sorts trees
496     # (including our chunked trees) as if their names ended with "/",
497     # so "fo" sorts after "fo." iff fo is a directory.  This makes
498     # streaming impossible when we need the metadata.
499     def result_from_tree_entry(tree_entry):
500         gitmode, mangled_name, oid = tree_entry
501         name, kind = git.demangle_name(mangled_name, gitmode)
502         return name, mangled_name, kind, gitmode, oid
503
504     tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
505     if bupm:
506         tree_ents = sorted(tree_ents, key=lambda x: x[0])
507     for ent in tree_ents:
508         yield ent
509     
510 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
511
512     def tree_item(ent_oid, kind, gitmode):
513         if kind == BUP_CHUNKED:
514             meta = Metadata.read(bupm) if bupm else default_file_mode
515             return Chunky(oid=ent_oid, meta=meta)
516
517         if S_ISDIR(gitmode):
518             # No metadata here (accessable via '.' inside ent_oid).
519             return Item(meta=default_dir_mode, oid=ent_oid)
520
521         return Item(oid=ent_oid,
522                     meta=(Metadata.read(bupm) if bupm \
523                           else _default_mode_for_gitmode(gitmode)))
524
525     assert len(oid) == 20
526     if not names:
527         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
528         yield '.', Item(oid=oid, meta=dot_meta)
529         tree_entries = ordered_tree_entries(tree_data, bupm)
530         for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
531             if mangled_name == '.bupm':
532                 continue
533             assert name != '.'
534             yield name, tree_item(ent_oid, kind, gitmode)
535         return
536
537     # Assumes the tree is properly formed, i.e. there are no
538     # duplicates, and entries will be in git tree order.
539     if type(names) not in (frozenset, set):
540         names = frozenset(names)
541     remaining = len(names)
542
543     # Account for the bupm sort order issue (cf. ordered_tree_entries above)
544     last_name = max(names) if bupm else max(names) + '/'
545
546     if '.' in names:
547         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
548         yield '.', Item(oid=oid, meta=dot_meta)
549         if remaining == 1:
550             return
551         remaining -= 1
552
553     tree_entries = ordered_tree_entries(tree_data, bupm)
554     for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
555         if mangled_name == '.bupm':
556             continue
557         assert name != '.'
558         if name not in names:
559             if name > last_name:
560                 break  # given bupm sort order, we're finished
561             if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
562                 Metadata.read(bupm)
563             continue
564         yield name, tree_item(ent_oid, kind, gitmode)
565         if remaining == 1:
566             break
567         remaining -= 1
568
569 def tree_items_with_meta(repo, oid, tree_data, names):
570     # For now, the .bupm order doesn't quite match git's, and we don't
571     # load the tree data incrementally anyway, so we just work in RAM
572     # via tree_data.
573     assert len(oid) == 20
574     bupm = None
575     for _, mangled_name, sub_oid in tree_decode(tree_data):
576         if mangled_name == '.bupm':
577             bupm = _FileReader(repo, sub_oid)
578             break
579         if mangled_name > '.bupm':
580             break
581     for item in tree_items(oid, tree_data, names, bupm):
582         yield item
583
584 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
585         
586 def _reverse_suffix_duplicates(strs):
587     """Yields the elements of strs, with any runs of duplicate values
588     suffixed with -N suffixes, where the zero padded integer N
589     decreases to 0 by 1 (e.g. 10, 09, ..., 00).
590
591     """
592     for name, duplicates in groupby(strs):
593         ndup = len(tuple(duplicates))
594         if ndup == 1:
595             yield name
596         else:
597             ndig = len(str(ndup - 1))
598             fmt = '%s-' + '%0' + str(ndig) + 'd'
599             for i in range(ndup - 1, -1, -1):
600                 yield fmt % (name, i)
601
602 def parse_rev(f):
603     items = f.readline().split(None)
604     assert len(items) == 2
605     tree, auth_sec = items
606     return tree.decode('hex'), int(auth_sec)
607
608 def _name_for_rev(rev):
609     commit_oidx, (tree_oid, utc) = rev
610     return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
611
612 def _item_for_rev(rev):
613     commit_oidx, (tree_oid, utc) = rev
614     coid = commit_oidx.decode('hex')
615     item = cache_get_commit_item(coid, need_meta=False)
616     if item:
617         return item
618     item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
619     cache_notice(item.coid, item)
620     return item
621
622 def cache_commit(repo, oid):
623     """Build, cache, and return a "name -> commit_item" dict of the entire
624     commit rev-list.
625
626     """
627     # For now, always cache with full metadata
628     entries = {}
629     entries['.'] = _revlist_item_from_oid(repo, oid, True)
630     revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
631                          parse=parse_rev)
632     rev_items, rev_names = tee(revs)
633     revs = None  # Don't disturb the tees
634     rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
635     rev_items = (_item_for_rev(x) for x in rev_items)
636     latest = None
637     for item in rev_items:
638         latest = latest or item
639         name = next(rev_names)
640         entries[name] = item
641     entries['latest'] = latest
642     cache_notice(latest.coid + b':r', entries)
643     return entries
644
645 def revlist_items(repo, oid, names):
646     assert len(oid) == 20
647
648     # Special case '.' instead of caching the whole history since it's
649     # the only way to get the metadata for the commit.
650     if names and all(x == '.' for x in names):
651         yield '.', _revlist_item_from_oid(repo, oid, True)
652         return
653
654     # For now, don't worry about the possibility of the contents being
655     # "too big" for the cache.
656     entries = cache_get(oid + b':r')
657     if not entries:
658         entries = cache_commit(repo, oid)
659
660     if not names:
661         for name in sorted(entries.keys()):
662             yield name, entries[name]
663         return
664
665     names = frozenset(name for name in names
666                       if _save_name_rx.match(name) or name in ('.', 'latest'))
667
668     if '.' in names:
669         yield '.', entries['.']
670     for name in (n for n in names if n != '.'):
671         commit = entries.get(name)
672         if commit:
673             yield name, commit
674
675 def tags_items(repo, names):
676     global _tags
677
678     def tag_item(oid):
679         assert len(oid) == 20
680         oidx = oid.encode('hex')
681         it = repo.cat(oidx)
682         _, typ, size = next(it)
683         if typ == 'commit':
684             return cache_get_commit_item(oid, need_meta=False) \
685                 or _commit_item_from_data(oid, ''.join(it))
686         for _ in it: pass
687         if typ == 'blob':
688             return Item(meta=default_file_mode, oid=oid)
689         elif typ == 'tree':
690             return Item(meta=default_dir_mode, oid=oid)
691         raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
692
693     if not names:
694         yield '.', _tags
695         # We have to pull these all into ram because tag_item calls cat()
696         for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
697             assert(name.startswith('refs/tags/'))
698             name = name[10:]
699             yield name, tag_item(oid)
700         return
701
702     # Assumes no duplicate refs
703     if type(names) not in (frozenset, set):
704         names = frozenset(names)
705     remaining = len(names)
706     last_name = max(names)
707     if '.' in names:
708         yield '.', _tags
709         if remaining == 1:
710             return
711         remaining -= 1
712
713     for name, oid in repo.refs(names, limit_to_tags=True):
714         assert(name.startswith('refs/tags/'))
715         name = name[10:]
716         if name > last_name:
717             return
718         if name not in names:
719             continue
720         yield name, tag_item(oid)
721         if remaining == 1:
722             return
723         remaining -= 1
724
725 def contents(repo, item, names=None, want_meta=True):
726     """Yields information about the items contained in item.  Yields
727     (name, item) for each name in names, if the name exists, in an
728     unspecified order.  If there are no names, then yields (name,
729     item) for all items, including, a first item named '.'
730     representing the container itself.
731
732     The meta value for any directories other than '.' will be a
733     default directory mode, not a Metadata object.  This is because
734     the actual metadata for a directory is stored inside the directory
735     (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
736
737     Note that want_meta is advisory.  For any given item, item.meta
738     might be a Metadata instance or a mode, and if the former,
739     meta.size might be None.  Missing sizes can be computed via via
740     item_size() or augment_item_meta(..., include_size=True).
741
742     Do not modify any item.meta Metadata instances directly.  If
743     needed, make a copy via item.meta.copy() and modify that instead.
744
745     """
746     # Q: are we comfortable promising '.' first when no names?
747     global _root, _tags
748     assert repo
749     assert S_ISDIR(item_mode(item))
750     item_t = type(item)
751     if item_t in real_tree_types:
752         it = repo.cat(item.oid.encode('hex'))
753         _, obj_t, size = next(it)
754         data = ''.join(it)
755         if obj_t != 'tree':
756             for _ in it: pass
757             # Note: it shouldn't be possible to see an Item with type
758             # 'commit' since a 'commit' should always produce a Commit.
759             raise Exception('unexpected git ' + obj_t)
760         if want_meta:
761             item_gen = tree_items_with_meta(repo, item.oid, data, names)
762         else:
763             item_gen = tree_items(item.oid, data, names)
764     elif item_t == RevList:
765         item_gen = revlist_items(repo, item.oid, names)
766     elif item_t == Root:
767         item_gen = root_items(repo, names, want_meta)
768     elif item_t == Tags:
769         item_gen = tags_items(repo, names)
770     else:
771         raise Exception('unexpected VFS item ' + str(item))
772     for x in item_gen:
773         yield x
774
775 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
776     cache_key = b'res:%d%d%d:%s\0%s' \
777                 % (bool(want_meta), bool(deref), repo.id(),
778                    ('/'.join(x[0] for x in parent) if parent else ''),
779                    '/'.join(path))
780     resolution = cache_get(cache_key)
781     if resolution:
782         return resolution
783
784     def notice_resolution(r):
785         cache_notice(cache_key, r)
786         return r
787
788     def raise_dir_required_but_not_dir(path, parent, past):
789         raise IOError(ENOTDIR,
790                       "path %r%s resolves to non-directory %r"
791                       % (path,
792                          ' (relative to %r)' % parent if parent else '',
793                          past),
794                       terminus=past)
795     global _root
796     assert repo
797     assert len(path)
798     if parent:
799         for x in parent:
800             assert len(x) == 2
801             assert type(x[0]) in (bytes, str)
802             assert type(x[1]) in item_types
803         assert parent[0][1] == _root
804         if not S_ISDIR(item_mode(parent[-1][1])):
805             raise IOError(ENOTDIR,
806                           'path resolution parent %r is not a directory'
807                           % (parent,))
808     is_absolute, must_be_dir, future = _decompose_path(path)
809     if must_be_dir:
810         deref = True
811     if not future:  # path was effectively '.' or '/'
812         if is_absolute:
813             return notice_resolution((('', _root),))
814         if parent:
815             return notice_resolution(tuple(parent))
816         return notice_resolution((('', _root),))
817     if is_absolute:
818         past = [('', _root)]
819     else:
820         past = list(parent) if parent else [('', _root)]
821     hops = 0
822     while True:
823         if not future:
824             if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
825                 raise_dir_required_but_not_dir(path, parent, past)
826             return notice_resolution(tuple(past))
827         segment = future.pop()
828         if segment == '..':
829             assert len(past) > 0
830             if len(past) > 1:  # .. from / is /
831                 assert S_ISDIR(item_mode(past[-1][1]))
832                 past.pop()
833         else:
834             parent_name, parent_item = past[-1]
835             wanted = (segment,) if not want_meta else ('.', segment)
836             items = tuple(contents(repo, parent_item, names=wanted,
837                                    want_meta=want_meta))
838             if not want_meta:
839                 item = items[0][1] if items else None
840             else:  # First item will be '.' and have the metadata
841                 item = items[1][1] if len(items) == 2 else None
842                 dot, dot_item = items[0]
843                 assert dot == '.'
844                 past[-1] = parent_name, parent_item
845             if not item:
846                 past.append((segment, None),)
847                 return notice_resolution(tuple(past))
848             mode = item_mode(item)
849             if not S_ISLNK(mode):
850                 if not S_ISDIR(mode):
851                     past.append((segment, item),)
852                     if future:
853                         raise IOError(ENOTDIR,
854                                       'path %r%s ends internally in non-directory here: %r'
855                                       % (path,
856                                          ' (relative to %r)' % parent if parent else '',
857                                          past),
858                                       terminus=past)
859                     if must_be_dir:
860                         raise_dir_required_but_not_dir(path, parent, past)
861                     return notice_resolution(tuple(past))
862                 # It's treeish
863                 if want_meta and type(item) in real_tree_types:
864                     dir_meta = _find_treeish_oid_metadata(repo, item.oid)
865                     if dir_meta:
866                         item = item._replace(meta=dir_meta)
867                 past.append((segment, item))
868             else:  # symlink
869                 if not future and not deref:
870                     past.append((segment, item),)
871                     continue
872                 if hops > 100:
873                     raise IOError(ELOOP,
874                                   'too many symlinks encountered while resolving %r%s'
875                                   % (path, ' relative to %r' % parent if parent else ''),
876                                   terminus=tuple(past + [(segment, item)]))
877                 target = readlink(repo, item)
878                 is_absolute, _, target_future = _decompose_path(target)
879                 if is_absolute:
880                     if not target_future:  # path was effectively '/'
881                         return notice_resolution((('', _root),))
882                     past = [('', _root)]
883                     future = target_future
884                 else:
885                     future.extend(target_future)
886                 hops += 1
887                 
888 def lresolve(repo, path, parent=None, want_meta=True):
889     """Perform exactly the same function as resolve(), except if the final
890     path element is a symbolic link, don't follow it, just return it
891     in the result.
892
893     """
894     return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
895                          deref=False)
896
897 def resolve(repo, path, parent=None, want_meta=True):
898     """Follow the path in the virtual filesystem and return a tuple
899     representing the location, if any, denoted by the path.  Each
900     element in the result tuple will be (name, info), where info will
901     be a VFS item that can be passed to functions like item_mode().
902
903     If a path segment that does not exist is encountered during
904     resolution, the result will represent the location of the missing
905     item, and that item in the result will be None.
906
907     Any attempt to traverse a non-directory will raise a VFS ENOTDIR
908     IOError exception.
909
910     Any symlinks along the path, including at the end, will be
911     resolved.  A VFS IOError with the errno attribute set to ELOOP
912     will be raised if too many symlinks are traversed while following
913     the path.  That exception is effectively like a normal
914     ELOOP IOError exception, but will include a terminus element
915     describing the location of the failure, which will be a tuple of
916     (name, info) elements.
917
918     The parent, if specified, must be a sequence of (name, item)
919     tuples, and will provide the starting point for the resolution of
920     the path.  If no parent is specified, resolution will start at
921     '/'.
922
923     The result may include elements of parent directly, so they must
924     not be modified later.  If this is a concern, pass in "name,
925     copy_item(item) for name, item in parent" instead.
926
927     When want_meta is true, detailed metadata will be included in each
928     result item if it's avaiable, otherwise item.meta will be an
929     integer mode.  The metadata size may or may not be provided, but
930     can be computed by item_size() or augment_item_meta(...,
931     include_size=True).  Setting want_meta=False is rarely desirable
932     since it can limit the VFS to just the metadata git itself can
933     represent, and so, as an example, fifos and sockets will appear to
934     be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
935     But the option is provided because it may be more efficient when
936     only the path names or the more limited metadata is sufficient.
937
938     Do not modify any item.meta Metadata instances directly.  If
939     needed, make a copy via item.meta.copy() and modify that instead.
940
941     """
942     result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
943                            deref=True)
944     _, leaf_item = result[-1]
945     if leaf_item:
946         assert not S_ISLNK(item_mode(leaf_item))
947     return result
948
949 def try_resolve(repo, path, parent=None, want_meta=True):
950     """If path does not refer to a symlink, does not exist, or refers to a
951     valid symlink, behave exactly like resolve().  If path refers to
952     an invalid symlink, behave like lresolve.
953
954     """
955     res = lresolve(repo, path, parent=parent, want_meta=want_meta)
956     leaf_name, leaf_item = res[-1]
957     if not leaf_item:
958         return res
959     if not S_ISLNK(item_mode(leaf_item)):
960         return res
961     deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
962     deref_name, deref_item = deref[-1]
963     if deref_item:
964         return deref
965     return res
966
967 def augment_item_meta(repo, item, include_size=False):
968     """Ensure item has a Metadata instance for item.meta.  If item.meta is
969     currently a mode, replace it with a compatible "fake" Metadata
970     instance.  If include_size is true, ensure item.meta.size is
971     correct, computing it if needed.  If item.meta is a Metadata
972     instance, this call may modify it in place or replace it.
973
974     """
975     # If we actually had parallelism, we'd need locking...
976     assert repo
977     m = item.meta
978     if isinstance(m, Metadata):
979         if include_size and m.size is None:
980             m.size = _compute_item_size(repo, item)
981             return item._replace(meta=m)
982         return item
983     # m is mode
984     meta = Metadata()
985     meta.mode = m
986     meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
987     if S_ISLNK(m):
988         target = _readlink(repo, item.oid)
989         meta.symlink_target = target
990         meta.size = len(target)
991     elif include_size:
992         meta.size = _compute_item_size(repo, item)
993     return item._replace(meta=meta)
994
995 def fill_in_metadata_if_dir(repo, item):
996     """If item is a directory and item.meta is not a Metadata instance,
997     attempt to find the metadata for the directory.  If found, return
998     a new item augmented to include that metadata.  Otherwise, return
999     item.  May be useful for the output of contents().
1000
1001     """
1002     if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
1003         items = tuple(contents(repo, item, ('.',), want_meta=True))
1004         assert len(items) == 1
1005         assert items[0][0] == '.'
1006         item = items[0][1]
1007     return item
1008
1009 def ensure_item_has_metadata(repo, item, include_size=False):
1010     """If item is a directory, attempt to find and add its metadata.  If
1011     the item still doesn't have a Metadata instance for item.meta,
1012     give it one via augment_item_meta().  May be useful for the output
1013     of contents().
1014
1015     """
1016     return augment_item_meta(repo,
1017                              fill_in_metadata_if_dir(repo, item),
1018                              include_size=include_size)