]> arthur.barton.de Git - bup.git/blob - lib/bup/vfs.py
vfs: use a type prefix for all cache keys
[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       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     return _readlink(repo, item.oid)
402
403 def _compute_item_size(repo, item):
404     mode = item_mode(item)
405     if S_ISREG(mode):
406         size = _normal_or_chunked_file_size(repo, item.oid)
407         return size
408     if S_ISLNK(mode):
409         return len(_readlink(repo, item.oid))
410     return 0
411
412 def item_size(repo, item):
413     """Return the size of item, computing it if necessary."""
414     m = item.meta
415     if isinstance(m, Metadata) and m.size is not None:
416         return m.size
417     return _compute_item_size(repo, item)
418
419 def tree_data_reader(repo, oid):
420     """Return an open reader for all of the data contained within oid.  If
421     oid refers to a tree, recursively concatenate all of its contents."""
422     return _FileReader(repo, oid)
423
424 def fopen(repo, item):
425     """Return an open reader for the given file item."""
426     assert S_ISREG(item_mode(item))
427     return tree_data_reader(repo, item.oid)
428
429 def _commit_item_from_data(oid, data):
430     info = parse_commit(data)
431     return Commit(meta=default_dir_mode,
432                   oid=info.tree.decode('hex'),
433                   coid=oid)
434
435 def _commit_item_from_oid(repo, oid, require_meta):
436     commit = cache_get_commit_item(oid, need_meta=require_meta)
437     if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
438         return commit
439     it = repo.cat(oid.encode('hex'))
440     _, typ, size = next(it)
441     assert typ == 'commit'
442     commit = _commit_item_from_data(oid, ''.join(it))
443     if require_meta:
444         meta = _find_treeish_oid_metadata(repo, commit.oid)
445         if meta:
446             commit = commit._replace(meta=meta)
447     commit_key = b'itm:' + oid
448     cache_notice(commit_key, commit)
449     return commit
450
451 def _revlist_item_from_oid(repo, oid, require_meta):
452     commit = _commit_item_from_oid(repo, oid, require_meta)
453     return RevList(oid=oid, meta=commit.meta)
454
455 def root_items(repo, names=None, want_meta=True):
456     """Yield (name, item) for the items in '/' in the VFS.  Return
457     everything if names is logically false, otherwise return only
458     items with a name in the collection.
459
460     """
461     # FIXME: what about non-leaf refs like 'refs/heads/foo/bar/baz?
462
463     global _root, _tags
464     if not names:
465         yield '.', _root
466         yield '.tag', _tags
467         # FIXME: maybe eventually support repo.clone() or something
468         # and pass in two repos, so we can drop the tuple() and stream
469         # in parallel (i.e. meta vs refs).
470         for name, oid in tuple(repo.refs([], limit_to_heads=True)):
471             assert(name.startswith('refs/heads/'))
472             yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
473         return
474
475     if '.' in names:
476         yield '.', _root
477     if '.tag' in names:
478         yield '.tag', _tags
479     for ref in names:
480         if ref in ('.', '.tag'):
481             continue
482         it = repo.cat('refs/heads/' + ref)
483         oidx, typ, size = next(it)
484         if not oidx:
485             for _ in it: pass
486             continue
487         assert typ == 'commit'
488         commit = parse_commit(''.join(it))
489         yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
490
491 def ordered_tree_entries(tree_data, bupm=None):
492     """Yields (name, mangled_name, kind, gitmode, oid) for each item in
493     tree, sorted by name.
494
495     """
496     # Sadly, the .bupm entries currently aren't in git tree order,
497     # i.e. they don't account for the fact that git sorts trees
498     # (including our chunked trees) as if their names ended with "/",
499     # so "fo" sorts after "fo." iff fo is a directory.  This makes
500     # streaming impossible when we need the metadata.
501     def result_from_tree_entry(tree_entry):
502         gitmode, mangled_name, oid = tree_entry
503         name, kind = git.demangle_name(mangled_name, gitmode)
504         return name, mangled_name, kind, gitmode, oid
505
506     tree_ents = (result_from_tree_entry(x) for x in tree_decode(tree_data))
507     if bupm:
508         tree_ents = sorted(tree_ents, key=lambda x: x[0])
509     for ent in tree_ents:
510         yield ent
511     
512 def tree_items(oid, tree_data, names=frozenset(), bupm=None):
513
514     def tree_item(ent_oid, kind, gitmode):
515         if kind == BUP_CHUNKED:
516             meta = Metadata.read(bupm) if bupm else default_file_mode
517             return Chunky(oid=ent_oid, meta=meta)
518
519         if S_ISDIR(gitmode):
520             # No metadata here (accessable via '.' inside ent_oid).
521             return Item(meta=default_dir_mode, oid=ent_oid)
522
523         return Item(oid=ent_oid,
524                     meta=(Metadata.read(bupm) if bupm \
525                           else _default_mode_for_gitmode(gitmode)))
526
527     assert len(oid) == 20
528     if not names:
529         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
530         yield '.', Item(oid=oid, meta=dot_meta)
531         tree_entries = ordered_tree_entries(tree_data, bupm)
532         for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
533             if mangled_name == '.bupm':
534                 continue
535             assert name != '.'
536             yield name, tree_item(ent_oid, kind, gitmode)
537         return
538
539     # Assumes the tree is properly formed, i.e. there are no
540     # duplicates, and entries will be in git tree order.
541     if type(names) not in (frozenset, set):
542         names = frozenset(names)
543     remaining = len(names)
544
545     # Account for the bupm sort order issue (cf. ordered_tree_entries above)
546     last_name = max(names) if bupm else max(names) + '/'
547
548     if '.' in names:
549         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
550         yield '.', Item(oid=oid, meta=dot_meta)
551         if remaining == 1:
552             return
553         remaining -= 1
554
555     tree_entries = ordered_tree_entries(tree_data, bupm)
556     for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
557         if mangled_name == '.bupm':
558             continue
559         assert name != '.'
560         if name not in names:
561             if name > last_name:
562                 break  # given bupm sort order, we're finished
563             if (kind == BUP_CHUNKED or not S_ISDIR(gitmode)) and bupm:
564                 Metadata.read(bupm)
565             continue
566         yield name, tree_item(ent_oid, kind, gitmode)
567         if remaining == 1:
568             break
569         remaining -= 1
570
571 def tree_items_with_meta(repo, oid, tree_data, names):
572     # For now, the .bupm order doesn't quite match git's, and we don't
573     # load the tree data incrementally anyway, so we just work in RAM
574     # via tree_data.
575     assert len(oid) == 20
576     bupm = None
577     for _, mangled_name, sub_oid in tree_decode(tree_data):
578         if mangled_name == '.bupm':
579             bupm = _FileReader(repo, sub_oid)
580             break
581         if mangled_name > '.bupm':
582             break
583     for item in tree_items(oid, tree_data, names, bupm):
584         yield item
585
586 _save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
587         
588 def _reverse_suffix_duplicates(strs):
589     """Yields the elements of strs, with any runs of duplicate values
590     suffixed with -N suffixes, where the zero padded integer N
591     decreases to 0 by 1 (e.g. 10, 09, ..., 00).
592
593     """
594     for name, duplicates in groupby(strs):
595         ndup = len(tuple(duplicates))
596         if ndup == 1:
597             yield name
598         else:
599             ndig = len(str(ndup - 1))
600             fmt = '%s-' + '%0' + str(ndig) + 'd'
601             for i in range(ndup - 1, -1, -1):
602                 yield fmt % (name, i)
603
604 def parse_rev(f):
605     items = f.readline().split(None)
606     assert len(items) == 2
607     tree, auth_sec = items
608     return tree.decode('hex'), int(auth_sec)
609
610 def _name_for_rev(rev):
611     commit_oidx, (tree_oid, utc) = rev
612     return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
613
614 def _item_for_rev(rev):
615     commit_oidx, (tree_oid, utc) = rev
616     coid = commit_oidx.decode('hex')
617     item = cache_get_commit_item(coid, need_meta=False)
618     if item:
619         return item
620     item = Commit(meta=default_dir_mode, oid=tree_oid, coid=coid)
621     commit_key = b'itm:' + coid
622     cache_notice(commit_key, item)
623     return item
624
625 def cache_commit(repo, oid):
626     """Build, cache, and return a "name -> commit_item" dict of the entire
627     commit rev-list.
628
629     """
630     # For now, always cache with full metadata
631     entries = {}
632     entries['.'] = _revlist_item_from_oid(repo, oid, True)
633     revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
634                          parse=parse_rev)
635     rev_items, rev_names = tee(revs)
636     revs = None  # Don't disturb the tees
637     rev_names = _reverse_suffix_duplicates(_name_for_rev(x) for x in rev_names)
638     rev_items = (_item_for_rev(x) for x in rev_items)
639     latest = None
640     for item in rev_items:
641         latest = latest or item
642         name = next(rev_names)
643         entries[name] = item
644     entries['latest'] = latest
645     revlist_key = b'rvl:' + latest.coid
646     cache_notice(revlist_key, entries)
647     return entries
648
649 def revlist_items(repo, oid, names):
650     assert len(oid) == 20
651
652     # Special case '.' instead of caching the whole history since it's
653     # the only way to get the metadata for the commit.
654     if names and all(x == '.' for x in names):
655         yield '.', _revlist_item_from_oid(repo, oid, True)
656         return
657
658     # For now, don't worry about the possibility of the contents being
659     # "too big" for the cache.
660     revlist_key = b'rvl:' + oid
661     entries = cache_get(revlist_key)
662     if not entries:
663         entries = cache_commit(repo, oid)
664
665     if not names:
666         for name in sorted(entries.keys()):
667             yield name, entries[name]
668         return
669
670     names = frozenset(name for name in names
671                       if _save_name_rx.match(name) or name in ('.', 'latest'))
672
673     if '.' in names:
674         yield '.', entries['.']
675     for name in (n for n in names if n != '.'):
676         commit = entries.get(name)
677         if commit:
678             yield name, commit
679
680 def tags_items(repo, names):
681     global _tags
682
683     def tag_item(oid):
684         assert len(oid) == 20
685         oidx = oid.encode('hex')
686         it = repo.cat(oidx)
687         _, typ, size = next(it)
688         if typ == 'commit':
689             return cache_get_commit_item(oid, need_meta=False) \
690                 or _commit_item_from_data(oid, ''.join(it))
691         for _ in it: pass
692         if typ == 'blob':
693             return Item(meta=default_file_mode, oid=oid)
694         elif typ == 'tree':
695             return Item(meta=default_dir_mode, oid=oid)
696         raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
697
698     if not names:
699         yield '.', _tags
700         # We have to pull these all into ram because tag_item calls cat()
701         for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
702             assert(name.startswith('refs/tags/'))
703             name = name[10:]
704             yield name, tag_item(oid)
705         return
706
707     # Assumes no duplicate refs
708     if type(names) not in (frozenset, set):
709         names = frozenset(names)
710     remaining = len(names)
711     last_name = max(names)
712     if '.' in names:
713         yield '.', _tags
714         if remaining == 1:
715             return
716         remaining -= 1
717
718     for name, oid in repo.refs(names, limit_to_tags=True):
719         assert(name.startswith('refs/tags/'))
720         name = name[10:]
721         if name > last_name:
722             return
723         if name not in names:
724             continue
725         yield name, tag_item(oid)
726         if remaining == 1:
727             return
728         remaining -= 1
729
730 def contents(repo, item, names=None, want_meta=True):
731     """Yields information about the items contained in item.  Yields
732     (name, item) for each name in names, if the name exists, in an
733     unspecified order.  If there are no names, then yields (name,
734     item) for all items, including, a first item named '.'
735     representing the container itself.
736
737     The meta value for any directories other than '.' will be a
738     default directory mode, not a Metadata object.  This is because
739     the actual metadata for a directory is stored inside the directory
740     (see fill_in_metadata_if_dir() or ensure_item_has_metadata()).
741
742     Note that want_meta is advisory.  For any given item, item.meta
743     might be a Metadata instance or a mode, and if the former,
744     meta.size might be None.  Missing sizes can be computed via via
745     item_size() or augment_item_meta(..., include_size=True).
746
747     Do not modify any item.meta Metadata instances directly.  If
748     needed, make a copy via item.meta.copy() and modify that instead.
749
750     """
751     # Q: are we comfortable promising '.' first when no names?
752     global _root, _tags
753     assert repo
754     assert S_ISDIR(item_mode(item))
755     item_t = type(item)
756     if item_t in real_tree_types:
757         it = repo.cat(item.oid.encode('hex'))
758         _, obj_t, size = next(it)
759         data = ''.join(it)
760         if obj_t != 'tree':
761             for _ in it: pass
762             # Note: it shouldn't be possible to see an Item with type
763             # 'commit' since a 'commit' should always produce a Commit.
764             raise Exception('unexpected git ' + obj_t)
765         if want_meta:
766             item_gen = tree_items_with_meta(repo, item.oid, data, names)
767         else:
768             item_gen = tree_items(item.oid, data, names)
769     elif item_t == RevList:
770         item_gen = revlist_items(repo, item.oid, names)
771     elif item_t == Root:
772         item_gen = root_items(repo, names, want_meta)
773     elif item_t == Tags:
774         item_gen = tags_items(repo, names)
775     else:
776         raise Exception('unexpected VFS item ' + str(item))
777     for x in item_gen:
778         yield x
779
780 def _resolve_path(repo, path, parent=None, want_meta=True, deref=False):
781     cache_key = b'res:%d%d%d:%s\0%s' \
782                 % (bool(want_meta), bool(deref), repo.id(),
783                    ('/'.join(x[0] for x in parent) if parent else ''),
784                    '/'.join(path))
785     resolution = cache_get(cache_key)
786     if resolution:
787         return resolution
788
789     def notice_resolution(r):
790         cache_notice(cache_key, r)
791         return r
792
793     def raise_dir_required_but_not_dir(path, parent, past):
794         raise IOError(ENOTDIR,
795                       "path %r%s resolves to non-directory %r"
796                       % (path,
797                          ' (relative to %r)' % parent if parent else '',
798                          past),
799                       terminus=past)
800     global _root
801     assert repo
802     assert len(path)
803     if parent:
804         for x in parent:
805             assert len(x) == 2
806             assert type(x[0]) in (bytes, str)
807             assert type(x[1]) in item_types
808         assert parent[0][1] == _root
809         if not S_ISDIR(item_mode(parent[-1][1])):
810             raise IOError(ENOTDIR,
811                           'path resolution parent %r is not a directory'
812                           % (parent,))
813     is_absolute, must_be_dir, future = _decompose_path(path)
814     if must_be_dir:
815         deref = True
816     if not future:  # path was effectively '.' or '/'
817         if is_absolute:
818             return notice_resolution((('', _root),))
819         if parent:
820             return notice_resolution(tuple(parent))
821         return notice_resolution((('', _root),))
822     if is_absolute:
823         past = [('', _root)]
824     else:
825         past = list(parent) if parent else [('', _root)]
826     hops = 0
827     while True:
828         if not future:
829             if must_be_dir and not S_ISDIR(item_mode(past[-1][1])):
830                 raise_dir_required_but_not_dir(path, parent, past)
831             return notice_resolution(tuple(past))
832         segment = future.pop()
833         if segment == '..':
834             assert len(past) > 0
835             if len(past) > 1:  # .. from / is /
836                 assert S_ISDIR(item_mode(past[-1][1]))
837                 past.pop()
838         else:
839             parent_name, parent_item = past[-1]
840             wanted = (segment,) if not want_meta else ('.', segment)
841             items = tuple(contents(repo, parent_item, names=wanted,
842                                    want_meta=want_meta))
843             if not want_meta:
844                 item = items[0][1] if items else None
845             else:  # First item will be '.' and have the metadata
846                 item = items[1][1] if len(items) == 2 else None
847                 dot, dot_item = items[0]
848                 assert dot == '.'
849                 past[-1] = parent_name, parent_item
850             if not item:
851                 past.append((segment, None),)
852                 return notice_resolution(tuple(past))
853             mode = item_mode(item)
854             if not S_ISLNK(mode):
855                 if not S_ISDIR(mode):
856                     past.append((segment, item),)
857                     if future:
858                         raise IOError(ENOTDIR,
859                                       'path %r%s ends internally in non-directory here: %r'
860                                       % (path,
861                                          ' (relative to %r)' % parent if parent else '',
862                                          past),
863                                       terminus=past)
864                     if must_be_dir:
865                         raise_dir_required_but_not_dir(path, parent, past)
866                     return notice_resolution(tuple(past))
867                 # It's treeish
868                 if want_meta and type(item) in real_tree_types:
869                     dir_meta = _find_treeish_oid_metadata(repo, item.oid)
870                     if dir_meta:
871                         item = item._replace(meta=dir_meta)
872                 past.append((segment, item))
873             else:  # symlink
874                 if not future and not deref:
875                     past.append((segment, item),)
876                     continue
877                 if hops > 100:
878                     raise IOError(ELOOP,
879                                   'too many symlinks encountered while resolving %r%s'
880                                   % (path, ' relative to %r' % parent if parent else ''),
881                                   terminus=tuple(past + [(segment, item)]))
882                 target = readlink(repo, item)
883                 is_absolute, _, target_future = _decompose_path(target)
884                 if is_absolute:
885                     if not target_future:  # path was effectively '/'
886                         return notice_resolution((('', _root),))
887                     past = [('', _root)]
888                     future = target_future
889                 else:
890                     future.extend(target_future)
891                 hops += 1
892                 
893 def lresolve(repo, path, parent=None, want_meta=True):
894     """Perform exactly the same function as resolve(), except if the final
895     path element is a symbolic link, don't follow it, just return it
896     in the result.
897
898     """
899     return _resolve_path(repo, path, parent=parent, want_meta=want_meta,
900                          deref=False)
901
902 def resolve(repo, path, parent=None, want_meta=True):
903     """Follow the path in the virtual filesystem and return a tuple
904     representing the location, if any, denoted by the path.  Each
905     element in the result tuple will be (name, info), where info will
906     be a VFS item that can be passed to functions like item_mode().
907
908     If a path segment that does not exist is encountered during
909     resolution, the result will represent the location of the missing
910     item, and that item in the result will be None.
911
912     Any attempt to traverse a non-directory will raise a VFS ENOTDIR
913     IOError exception.
914
915     Any symlinks along the path, including at the end, will be
916     resolved.  A VFS IOError with the errno attribute set to ELOOP
917     will be raised if too many symlinks are traversed while following
918     the path.  That exception is effectively like a normal
919     ELOOP IOError exception, but will include a terminus element
920     describing the location of the failure, which will be a tuple of
921     (name, info) elements.
922
923     The parent, if specified, must be a sequence of (name, item)
924     tuples, and will provide the starting point for the resolution of
925     the path.  If no parent is specified, resolution will start at
926     '/'.
927
928     The result may include elements of parent directly, so they must
929     not be modified later.  If this is a concern, pass in "name,
930     copy_item(item) for name, item in parent" instead.
931
932     When want_meta is true, detailed metadata will be included in each
933     result item if it's avaiable, otherwise item.meta will be an
934     integer mode.  The metadata size may or may not be provided, but
935     can be computed by item_size() or augment_item_meta(...,
936     include_size=True).  Setting want_meta=False is rarely desirable
937     since it can limit the VFS to just the metadata git itself can
938     represent, and so, as an example, fifos and sockets will appear to
939     be regular files (e.g. S_ISREG(item_mode(item)) will be true) .
940     But the option is provided because it may be more efficient when
941     only the path names or the more limited metadata is sufficient.
942
943     Do not modify any item.meta Metadata instances directly.  If
944     needed, make a copy via item.meta.copy() and modify that instead.
945
946     """
947     result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
948                            deref=True)
949     _, leaf_item = result[-1]
950     if leaf_item:
951         assert not S_ISLNK(item_mode(leaf_item))
952     return result
953
954 def try_resolve(repo, path, parent=None, want_meta=True):
955     """If path does not refer to a symlink, does not exist, or refers to a
956     valid symlink, behave exactly like resolve().  If path refers to
957     an invalid symlink, behave like lresolve.
958
959     """
960     res = lresolve(repo, path, parent=parent, want_meta=want_meta)
961     leaf_name, leaf_item = res[-1]
962     if not leaf_item:
963         return res
964     if not S_ISLNK(item_mode(leaf_item)):
965         return res
966     deref = resolve(repo, leaf_name, parent=res[:-1], want_meta=want_meta)
967     deref_name, deref_item = deref[-1]
968     if deref_item:
969         return deref
970     return res
971
972 def augment_item_meta(repo, item, include_size=False):
973     """Ensure item has a Metadata instance for item.meta.  If item.meta is
974     currently a mode, replace it with a compatible "fake" Metadata
975     instance.  If include_size is true, ensure item.meta.size is
976     correct, computing it if needed.  If item.meta is a Metadata
977     instance, this call may modify it in place or replace it.
978
979     """
980     # If we actually had parallelism, we'd need locking...
981     assert repo
982     m = item.meta
983     if isinstance(m, Metadata):
984         if include_size and m.size is None:
985             m.size = _compute_item_size(repo, item)
986             return item._replace(meta=m)
987         return item
988     # m is mode
989     meta = Metadata()
990     meta.mode = m
991     meta.uid = meta.gid = meta.atime = meta.mtime = meta.ctime = 0
992     if S_ISLNK(m):
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)