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