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