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