]> arthur.barton.de Git - bup.git/blobdiff - lib/bup/vfs.py
vfs: remove dead cache_get_revlist_item()
[bup.git] / lib / bup / vfs.py
index e3ea16ffae505d95c28a8d61ad7238f98730a94c..8b4789c02e151e508082684131e66229f8c34352 100644 (file)
@@ -47,26 +47,59 @@ item.coid.
 """
 
 from __future__ import absolute_import, print_function
 """
 
 from __future__ import absolute_import, print_function
+from binascii import hexlify, unhexlify
 from collections import namedtuple
 from errno import EINVAL, ELOOP, ENOENT, ENOTDIR
 from itertools import chain, dropwhile, groupby, tee
 from random import randrange
 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
 from time import localtime, strftime
 from collections import namedtuple
 from errno import EINVAL, ELOOP, ENOENT, ENOTDIR
 from itertools import chain, dropwhile, groupby, tee
 from random import randrange
 from stat import S_IFDIR, S_IFLNK, S_IFREG, S_ISDIR, S_ISLNK, S_ISREG
 from time import localtime, strftime
-import exceptions, re, sys
+import re, sys
 
 
-from bup import client, git, metadata
-from bup.compat import range
+from bup import git, metadata, vint
+from bup.compat import hexstr, range
 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
 from bup.helpers import debug2, last
 from bup.git import BUP_CHUNKED, cp, get_commit_items, parse_commit, tree_decode
 from bup.helpers import debug2, last
+from bup.io import path_msg
 from bup.metadata import Metadata
 from bup.metadata import Metadata
+from bup.vint import read_bvec, write_bvec
+from bup.vint import read_vint, write_vint
+from bup.vint import read_vuint, write_vuint
 
 
+if sys.version_info[0] < 3:
+    from exceptions import IOError as py_IOError
+else:
+    py_IOError = IOError
 
 
-class IOError(exceptions.IOError):
+# We currently assume that it's always appropriate to just forward IOErrors
+# to a remote client.
+
+class IOError(py_IOError):
     def __init__(self, errno, message, terminus=None):
     def __init__(self, errno, message, terminus=None):
-        exceptions.IOError.__init__(self, errno, message)
+        py_IOError.__init__(self, errno, message)
         self.terminus = terminus
 
         self.terminus = terminus
 
+def write_ioerror(port, ex):
+    assert isinstance(ex, IOError)
+    write_vuint(port,
+                (1 if ex.errno is not None else 0)
+                | (2 if ex.strerror is not None else 0)
+                | (4 if ex.terminus is not None else 0))
+    if ex.errno is not None:
+        write_vint(port, ex.errno)
+    if ex.strerror is not None:
+        write_bvec(port, ex.strerror.encode('utf-8'))
+    if ex.terminus is not None:
+        write_resolution(port, ex.terminus)
+
+def read_ioerror(port):
+    mask = read_vuint(port)
+    no = read_vint(port) if 1 & mask else None
+    msg = read_bvec(port).decode('utf-8') if 2 & mask else None
+    term = read_resolution(port) if 4 & mask else None
+    return IOError(errno=no, message=msg, terminus=term)
+
+
 default_file_mode = S_IFREG | 0o644
 default_dir_mode = S_IFDIR | 0o755
 default_symlink_mode = S_IFLNK | 0o755
 default_file_mode = S_IFREG | 0o644
 default_dir_mode = S_IFDIR | 0o755
 default_symlink_mode = S_IFLNK | 0o755
@@ -83,13 +116,13 @@ def _default_mode_for_gitmode(gitmode):
 def _normal_or_chunked_file_size(repo, oid):
     """Return the size of the normal or chunked file indicated by oid."""
     # FIXME: --batch-format CatPipe?
 def _normal_or_chunked_file_size(repo, oid):
     """Return the size of the normal or chunked file indicated by oid."""
     # FIXME: --batch-format CatPipe?
-    it = repo.cat(oid.encode('hex'))
+    it = repo.cat(hexlify(oid))
     _, obj_t, size = next(it)
     ofs = 0
     _, obj_t, size = next(it)
     ofs = 0
-    while obj_t == 'tree':
-        mode, name, last_oid = last(tree_decode(''.join(it)))
+    while obj_t == b'tree':
+        mode, name, last_oid = last(tree_decode(b''.join(it)))
         ofs += int(name, 16)
         ofs += int(name, 16)
-        it = repo.cat(last_oid.encode('hex'))
+        it = repo.cat(hexlify(last_oid))
         _, obj_t, size = next(it)
     return ofs + sum(len(b) for b in it)
 
         _, obj_t, size = next(it)
     return ofs + sum(len(b) for b in it)
 
@@ -116,23 +149,23 @@ def _tree_chunks(repo, tree, startofs):
         skipmore = startofs - ofs
         if skipmore < 0:
             skipmore = 0
         skipmore = startofs - ofs
         if skipmore < 0:
             skipmore = 0
-        it = repo.cat(oid.encode('hex'))
+        it = repo.cat(hexlify(oid))
         _, obj_t, size = next(it)
         _, obj_t, size = next(it)
-        data = ''.join(it)            
+        data = b''.join(it)
         if S_ISDIR(mode):
         if S_ISDIR(mode):
-            assert obj_t == 'tree'
+            assert obj_t == b'tree'
             for b in _tree_chunks(repo, tree_decode(data), skipmore):
                 yield b
         else:
             for b in _tree_chunks(repo, tree_decode(data), skipmore):
                 yield b
         else:
-            assert obj_t == 'blob'
+            assert obj_t == b'blob'
             yield data[skipmore:]
 
 class _ChunkReader:
     def __init__(self, repo, oid, startofs):
             yield data[skipmore:]
 
 class _ChunkReader:
     def __init__(self, repo, oid, startofs):
-        it = repo.cat(oid.encode('hex'))
+        it = repo.cat(hexlify(oid))
         _, obj_t, size = next(it)
         _, obj_t, size = next(it)
-        isdir = obj_t == 'tree'
-        data = ''.join(it)
+        isdir = obj_t == b'tree'
+        data = b''.join(it)
         if isdir:
             self.it = _tree_chunks(repo, tree_decode(data), startofs)
             self.blob = None
         if isdir:
             self.it = _tree_chunks(repo, tree_decode(data), startofs)
             self.blob = None
@@ -142,11 +175,11 @@ class _ChunkReader:
         self.ofs = startofs
 
     def next(self, size):
         self.ofs = startofs
 
     def next(self, size):
-        out = ''
+        out = b''
         while len(out) < size:
             if self.it and not self.blob:
                 try:
         while len(out) < size:
             if self.it and not self.blob:
                 try:
-                    self.blob = self.it.next()
+                    self.blob = next(self.it)
                 except StopIteration:
                     self.it = None
             if self.blob:
                 except StopIteration:
                     self.it = None
             if self.blob:
@@ -184,7 +217,7 @@ class _FileReader(object):
     def read(self, count=-1):
         size = self._compute_size()
         if self.ofs >= size:
     def read(self, count=-1):
         size = self._compute_size()
         if self.ofs >= size:
-            return ''
+            return b''
         if count < 0:
             count = size - self.ofs
         if not self.reader or self.reader.ofs != self.ofs:
         if count < 0:
             count = size - self.ofs
         if not self.reader or self.reader.ofs != self.ofs:
@@ -206,7 +239,7 @@ class _FileReader(object):
         self.close()
         return False
 
         self.close()
         return False
 
-_multiple_slashes_rx = re.compile(r'//+')
+_multiple_slashes_rx = re.compile(br'//+')
 
 def _decompose_path(path):
     """Return a boolean indicating whether the path is absolute, and a
 
 def _decompose_path(path):
     """Return a boolean indicating whether the path is absolute, and a
@@ -215,18 +248,18 @@ def _decompose_path(path):
     effectively '/' or '.', return an empty list.
 
     """
     effectively '/' or '.', return an empty list.
 
     """
-    path = re.sub(_multiple_slashes_rx, '/', path)
-    if path == '/':
+    path = re.sub(_multiple_slashes_rx, b'/', path)
+    if path == b'/':
         return True, True, []
     is_absolute = must_be_dir = False
         return True, True, []
     is_absolute = must_be_dir = False
-    if path.startswith('/'):
+    if path.startswith(b'/'):
         is_absolute = True
         path = path[1:]
         is_absolute = True
         path = path[1:]
-    for suffix in ('/', '/.'):
+    for suffix in (b'/', b'/.'):
         if path.endswith(suffix):
             must_be_dir = True
             path = path[:-len(suffix)]
         if path.endswith(suffix):
             must_be_dir = True
             path = path[:-len(suffix)]
-    parts = [x for x in path.split('/') if x != '.']
+    parts = [x for x in path.split(b'/') if x != b'.']
     parts.reverse()
     if not parts:
         must_be_dir = True  # e.g. path was effectively '.' or '/', etc.
     parts.reverse()
     if not parts:
         must_be_dir = True  # e.g. path was effectively '.' or '/', etc.
@@ -244,6 +277,93 @@ Commit = namedtuple('Commit', ('meta', 'oid', 'coid'))
 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
 real_tree_types = frozenset((Item, Commit))
 
 item_types = frozenset((Item, Chunky, Root, Tags, RevList, Commit))
 real_tree_types = frozenset((Item, Commit))
 
+def write_item(port, item):
+    kind = type(item)
+    name = bytes(kind.__name__.encode('ascii'))
+    meta = item.meta
+    has_meta = 1 if isinstance(meta, Metadata) else 0
+    if kind in (Item, Chunky, RevList):
+        assert len(item.oid) == 20
+        if has_meta:
+            vint.send(port, 'sVs', name, has_meta, item.oid)
+            Metadata.write(meta, port, include_path=False)
+        else:
+            vint.send(port, 'sVsV', name, has_meta, item.oid, item.meta)
+    elif kind in (Root, Tags):
+        if has_meta:
+            vint.send(port, 'sV', name, has_meta)
+            Metadata.write(meta, port, include_path=False)
+        else:
+            vint.send(port, 'sVV', name, has_meta, item.meta)
+    elif kind == Commit:
+        assert len(item.oid) == 20
+        assert len(item.coid) == 20
+        if has_meta:
+            vint.send(port, 'sVss', name, has_meta, item.oid, item.coid)
+            Metadata.write(meta, port, include_path=False)
+        else:
+            vint.send(port, 'sVssV', name, has_meta, item.oid, item.coid,
+                      item.meta)
+    elif kind == FakeLink:
+        if has_meta:
+            vint.send(port, 'sVs', name, has_meta, item.target)
+            Metadata.write(meta, port, include_path=False)
+        else:
+            vint.send(port, 'sVsV', name, has_meta, item.target, item.meta)
+    else:
+        assert False
+
+def read_item(port):
+    def read_m(port, has_meta):
+        if has_meta:
+            m = Metadata.read(port)
+            return m
+        return read_vuint(port)
+    kind, has_meta = vint.recv(port, 'sV')
+    if kind == b'Item':
+        oid, meta = read_bvec(port), read_m(port, has_meta)
+        return Item(oid=oid, meta=meta)
+    if kind == b'Chunky':
+        oid, meta = read_bvec(port), read_m(port, has_meta)
+        return Chunky(oid=oid, meta=meta)
+    if kind == b'RevList':
+        oid, meta = read_bvec(port), read_m(port, has_meta)
+        return RevList(oid=oid, meta=meta)
+    if kind == b'Root':
+        return Root(meta=read_m(port, has_meta))
+    if kind == b'Tags':
+        return Tags(meta=read_m(port, has_meta))
+    if kind == b'Commit':
+        oid, coid = vint.recv(port, 'ss')
+        meta = read_m(port, has_meta)
+        return Commit(oid=oid, coid=coid, meta=meta)
+    if kind == b'FakeLink':
+        target, meta = read_bvec(port), read_m(port, has_meta)
+        return FakeLink(target=target, meta=meta)
+    assert False
+
+def write_resolution(port, resolution):
+    write_vuint(port, len(resolution))
+    for name, item in resolution:
+        write_bvec(port, name)
+        if item:
+            port.write(b'\x01')
+            write_item(port, item)
+        else:
+            port.write(b'\x00')
+
+def read_resolution(port):
+    n = read_vuint(port)
+    result = []
+    for i in range(n):
+        name = read_bvec(port)
+        have_item = ord(port.read(1))
+        assert have_item in (0, 1)
+        item = read_item(port) if have_item else None
+        result.append((name, item))
+    return tuple(result)
+
+
 _root = Root(meta=default_dir_mode)
 _tags = Tags(meta=default_dir_mode)
 
 _root = Root(meta=default_dir_mode)
 _tags = Tags(meta=default_dir_mode)
 
@@ -275,9 +395,9 @@ def is_valid_cache_key(x):
     x_t = type(x)
     if x_t is bytes:
         tag = x[:4]
     x_t = type(x)
     if x_t is bytes:
         tag = x[:4]
-        if tag in ('itm:', 'rvl:') and len(x) == 24:
+        if tag in (b'itm:', b'rvl:') and len(x) == 24:
             return True
             return True
-        if tag == 'res:':
+        if tag == b'res:':
             return True
 
 def cache_get(key):
             return True
 
 def cache_get(key):
@@ -316,12 +436,7 @@ def cache_get_commit_item(oid, need_meta=True):
             return item
     entries = cache_get(b'rvl:' + oid)
     if entries:
             return item
     entries = cache_get(b'rvl:' + oid)
     if entries:
-        return entries['.']
-
-def cache_get_revlist_item(oid, need_meta=True):
-    commit = cache_get_commit_item(oid, need_meta=need_meta)
-    if commit:
-        return RevList(oid=oid, meta=commit.meta)
+        return entries[b'.']
 
 def copy_item(item):
     """Return a completely independent copy of item, such that
 
 def copy_item(item):
     """Return a completely independent copy of item, such that
@@ -357,21 +472,21 @@ def tree_data_and_bupm(repo, oid):
 
     """    
     assert len(oid) == 20
 
     """    
     assert len(oid) == 20
-    it = repo.cat(oid.encode('hex'))
+    it = repo.cat(hexlify(oid))
     _, item_t, size = next(it)
     _, item_t, size = next(it)
-    data = ''.join(it)
-    if item_t == 'commit':
+    data = b''.join(it)
+    if item_t == b'commit':
         commit = parse_commit(data)
         it = repo.cat(commit.tree)
         _, item_t, size = next(it)
         commit = parse_commit(data)
         it = repo.cat(commit.tree)
         _, item_t, size = next(it)
-        data = ''.join(it)
-        assert item_t == 'tree'
-    elif item_t != 'tree':
-        raise Exception('%r is not a tree or commit' % oid.encode('hex'))
+        data = b''.join(it)
+        assert item_t == b'tree'
+    elif item_t != b'tree':
+        raise Exception('%s is not a tree or commit' % hexstr(oid))
     for _, mangled_name, sub_oid in tree_decode(data):
     for _, mangled_name, sub_oid in tree_decode(data):
-        if mangled_name == '.bupm':
+        if mangled_name == b'.bupm':
             return data, sub_oid
             return data, sub_oid
-        if mangled_name > '.bupm':
+        if mangled_name > b'.bupm':
             break
     return data, None
 
             break
     return data, None
 
@@ -387,19 +502,19 @@ def _find_treeish_oid_metadata(repo, oid):
     return None
 
 def _readlink(repo, oid):
     return None
 
 def _readlink(repo, oid):
-    return ''.join(repo.join(oid.encode('hex')))
+    return b''.join(repo.join(hexlify(oid)))
 
 def readlink(repo, item):
     """Return the link target of item, which must be a symlink.  Reads the
     target from the repository if necessary."""
     assert repo
     assert S_ISLNK(item_mode(item))
 
 def readlink(repo, item):
     """Return the link target of item, which must be a symlink.  Reads the
     target from the repository if necessary."""
     assert repo
     assert S_ISLNK(item_mode(item))
+    if isinstance(item, FakeLink):
+        return item.target
     if isinstance(item.meta, Metadata):
         target = item.meta.symlink_target
         if target:
             return target
     if isinstance(item.meta, Metadata):
         target = item.meta.symlink_target
         if target:
             return target
-    elif isinstance(item, FakeLink):
-        return item.target
     return _readlink(repo, item.oid)
 
 def _compute_item_size(repo, item):
     return _readlink(repo, item.oid)
 
 def _compute_item_size(repo, item):
@@ -408,6 +523,8 @@ def _compute_item_size(repo, item):
         size = _normal_or_chunked_file_size(repo, item.oid)
         return size
     if S_ISLNK(mode):
         size = _normal_or_chunked_file_size(repo, item.oid)
         return size
     if S_ISLNK(mode):
+        if isinstance(item, FakeLink):
+            return len(item.target)
         return len(_readlink(repo, item.oid))
     return 0
 
         return len(_readlink(repo, item.oid))
     return 0
 
@@ -431,17 +548,17 @@ def fopen(repo, item):
 def _commit_item_from_data(oid, data):
     info = parse_commit(data)
     return Commit(meta=default_dir_mode,
 def _commit_item_from_data(oid, data):
     info = parse_commit(data)
     return Commit(meta=default_dir_mode,
-                  oid=info.tree.decode('hex'),
+                  oid=unhexlify(info.tree),
                   coid=oid)
 
 def _commit_item_from_oid(repo, oid, require_meta):
     commit = cache_get_commit_item(oid, need_meta=require_meta)
     if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
         return commit
                   coid=oid)
 
 def _commit_item_from_oid(repo, oid, require_meta):
     commit = cache_get_commit_item(oid, need_meta=require_meta)
     if commit and ((not require_meta) or isinstance(commit.meta, Metadata)):
         return commit
-    it = repo.cat(oid.encode('hex'))
+    it = repo.cat(hexlify(oid))
     _, typ, size = next(it)
     _, typ, size = next(it)
-    assert typ == 'commit'
-    commit = _commit_item_from_data(oid, ''.join(it))
+    assert typ == b'commit'
+    commit = _commit_item_from_data(oid, b''.join(it))
     if require_meta:
         meta = _find_treeish_oid_metadata(repo, commit.oid)
         if meta:
     if require_meta:
         meta = _find_treeish_oid_metadata(repo, commit.oid)
         if meta:
@@ -464,31 +581,31 @@ def root_items(repo, names=None, want_meta=True):
 
     global _root, _tags
     if not names:
 
     global _root, _tags
     if not names:
-        yield '.', _root
-        yield '.tag', _tags
+        yield b'.', _root
+        yield b'.tag', _tags
         # FIXME: maybe eventually support repo.clone() or something
         # and pass in two repos, so we can drop the tuple() and stream
         # in parallel (i.e. meta vs refs).
         for name, oid in tuple(repo.refs([], limit_to_heads=True)):
         # FIXME: maybe eventually support repo.clone() or something
         # and pass in two repos, so we can drop the tuple() and stream
         # in parallel (i.e. meta vs refs).
         for name, oid in tuple(repo.refs([], limit_to_heads=True)):
-            assert(name.startswith('refs/heads/'))
+            assert(name.startswith(b'refs/heads/'))
             yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
         return
 
             yield name[11:], _revlist_item_from_oid(repo, oid, want_meta)
         return
 
-    if '.' in names:
-        yield '.', _root
-    if '.tag' in names:
-        yield '.tag', _tags
+    if b'.' in names:
+        yield b'.', _root
+    if b'.tag' in names:
+        yield b'.tag', _tags
     for ref in names:
     for ref in names:
-        if ref in ('.', '.tag'):
+        if ref in (b'.', b'.tag'):
             continue
             continue
-        it = repo.cat('refs/heads/' + ref)
+        it = repo.cat(b'refs/heads/' + ref)
         oidx, typ, size = next(it)
         if not oidx:
             for _ in it: pass
             continue
         oidx, typ, size = next(it)
         if not oidx:
             for _ in it: pass
             continue
-        assert typ == 'commit'
-        commit = parse_commit(''.join(it))
-        yield ref, _revlist_item_from_oid(repo, oidx.decode('hex'), want_meta)
+        assert typ == b'commit'
+        commit = parse_commit(b''.join(it))
+        yield ref, _revlist_item_from_oid(repo, unhexlify(oidx), want_meta)
 
 def ordered_tree_entries(tree_data, bupm=None):
     """Yields (name, mangled_name, kind, gitmode, oid) for each item in
 
 def ordered_tree_entries(tree_data, bupm=None):
     """Yields (name, mangled_name, kind, gitmode, oid) for each item in
@@ -496,10 +613,12 @@ def ordered_tree_entries(tree_data, bupm=None):
 
     """
     # Sadly, the .bupm entries currently aren't in git tree order,
 
     """
     # Sadly, the .bupm entries currently aren't in git tree order,
-    # i.e. they don't account for the fact that git sorts trees
-    # (including our chunked trees) as if their names ended with "/",
-    # so "fo" sorts after "fo." iff fo is a directory.  This makes
-    # streaming impossible when we need the metadata.
+    # but in unmangled name order. They _do_ account for the fact
+    # that git sorts trees (including chunked trees) as if their
+    # names ended with "/" (so "fo" sorts after "fo." iff fo is a
+    # directory), but we apply this on the unmangled names in save
+    # rather than on the mangled names.
+    # This makes streaming impossible when we need the metadata.
     def result_from_tree_entry(tree_entry):
         gitmode, mangled_name, oid = tree_entry
         name, kind = git.demangle_name(mangled_name, gitmode)
     def result_from_tree_entry(tree_entry):
         gitmode, mangled_name, oid = tree_entry
         name, kind = git.demangle_name(mangled_name, gitmode)
@@ -522,19 +641,22 @@ def tree_items(oid, tree_data, names=frozenset(), bupm=None):
             # No metadata here (accessable via '.' inside ent_oid).
             return Item(meta=default_dir_mode, oid=ent_oid)
 
             # No metadata here (accessable via '.' inside ent_oid).
             return Item(meta=default_dir_mode, oid=ent_oid)
 
-        return Item(oid=ent_oid,
-                    meta=(Metadata.read(bupm) if bupm \
-                          else _default_mode_for_gitmode(gitmode)))
+        meta = Metadata.read(bupm) if bupm else None
+        # handle the case of metadata being empty/missing in bupm
+        # (or there not being bupm at all)
+        if meta is None:
+            meta = _default_mode_for_gitmode(gitmode)
+        return Item(oid=ent_oid, meta=meta)
 
     assert len(oid) == 20
     if not names:
         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
 
     assert len(oid) == 20
     if not names:
         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
-        yield '.', Item(oid=oid, meta=dot_meta)
+        yield b'.', Item(oid=oid, meta=dot_meta)
         tree_entries = ordered_tree_entries(tree_data, bupm)
         for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
         tree_entries = ordered_tree_entries(tree_data, bupm)
         for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
-            if mangled_name == '.bupm':
+            if mangled_name == b'.bupm':
                 continue
                 continue
-            assert name != '.'
+            assert name != b'.'
             yield name, tree_item(ent_oid, kind, gitmode)
         return
 
             yield name, tree_item(ent_oid, kind, gitmode)
         return
 
@@ -545,20 +667,20 @@ def tree_items(oid, tree_data, names=frozenset(), bupm=None):
     remaining = len(names)
 
     # Account for the bupm sort order issue (cf. ordered_tree_entries above)
     remaining = len(names)
 
     # Account for the bupm sort order issue (cf. ordered_tree_entries above)
-    last_name = max(names) if bupm else max(names) + '/'
+    last_name = max(names) if bupm else max(names) + b'/'
 
 
-    if '.' in names:
+    if b'.' in names:
         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
         dot_meta = _read_dir_meta(bupm) if bupm else default_dir_mode
-        yield '.', Item(oid=oid, meta=dot_meta)
+        yield b'.', Item(oid=oid, meta=dot_meta)
         if remaining == 1:
             return
         remaining -= 1
 
     tree_entries = ordered_tree_entries(tree_data, bupm)
     for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
         if remaining == 1:
             return
         remaining -= 1
 
     tree_entries = ordered_tree_entries(tree_data, bupm)
     for name, mangled_name, kind, gitmode, ent_oid in tree_entries:
-        if mangled_name == '.bupm':
+        if mangled_name == b'.bupm':
             continue
             continue
-        assert name != '.'
+        assert name != b'.'
         if name not in names:
             if name > last_name:
                 break  # given bupm sort order, we're finished
         if name not in names:
             if name > last_name:
                 break  # given bupm sort order, we're finished
@@ -577,15 +699,15 @@ def tree_items_with_meta(repo, oid, tree_data, names):
     assert len(oid) == 20
     bupm = None
     for _, mangled_name, sub_oid in tree_decode(tree_data):
     assert len(oid) == 20
     bupm = None
     for _, mangled_name, sub_oid in tree_decode(tree_data):
-        if mangled_name == '.bupm':
+        if mangled_name == b'.bupm':
             bupm = _FileReader(repo, sub_oid)
             break
             bupm = _FileReader(repo, sub_oid)
             break
-        if mangled_name > '.bupm':
+        if mangled_name > b'.bupm':
             break
     for item in tree_items(oid, tree_data, names, bupm):
         yield item
 
             break
     for item in tree_items(oid, tree_data, names, bupm):
         yield item
 
-_save_name_rx = re.compile(r'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
+_save_name_rx = re.compile(br'^\d\d\d\d-\d\d-\d\d-\d{6}(-\d+)?$')
         
 def _reverse_suffix_duplicates(strs):
     """Yields the elements of strs, with any runs of duplicate values
         
 def _reverse_suffix_duplicates(strs):
     """Yields the elements of strs, with any runs of duplicate values
@@ -599,7 +721,7 @@ def _reverse_suffix_duplicates(strs):
             yield name
         else:
             ndig = len(str(ndup - 1))
             yield name
         else:
             ndig = len(str(ndup - 1))
-            fmt = '%s-' + '%0' + str(ndig) + 'd'
+            fmt = b'%s-' + b'%0' + (b'%d' % ndig) + b'd'
             for i in range(ndup - 1, -1, -1):
                 yield fmt % (name, i)
 
             for i in range(ndup - 1, -1, -1):
                 yield fmt % (name, i)
 
@@ -607,15 +729,15 @@ def parse_rev(f):
     items = f.readline().split(None)
     assert len(items) == 2
     tree, auth_sec = items
     items = f.readline().split(None)
     assert len(items) == 2
     tree, auth_sec = items
-    return tree.decode('hex'), int(auth_sec)
+    return unhexlify(tree), int(auth_sec)
 
 def _name_for_rev(rev):
     commit_oidx, (tree_oid, utc) = rev
 
 def _name_for_rev(rev):
     commit_oidx, (tree_oid, utc) = rev
-    return strftime('%Y-%m-%d-%H%M%S', localtime(utc))
+    return strftime('%Y-%m-%d-%H%M%S', localtime(utc)).encode('ascii')
 
 def _item_for_rev(rev):
     commit_oidx, (tree_oid, utc) = rev
 
 def _item_for_rev(rev):
     commit_oidx, (tree_oid, utc) = rev
-    coid = commit_oidx.decode('hex')
+    coid = unhexlify(commit_oidx)
     item = cache_get_commit_item(coid, need_meta=False)
     if item:
         return item
     item = cache_get_commit_item(coid, need_meta=False)
     if item:
         return item
@@ -631,8 +753,8 @@ def cache_commit(repo, oid):
     """
     # For now, always cache with full metadata
     entries = {}
     """
     # For now, always cache with full metadata
     entries = {}
-    entries['.'] = _revlist_item_from_oid(repo, oid, True)
-    revs = repo.rev_list((oid.encode('hex'),), format='%T %at',
+    entries[b'.'] = _revlist_item_from_oid(repo, oid, True)
+    revs = repo.rev_list((hexlify(oid),), format=b'%T %at',
                          parse=parse_rev)
     rev_items, rev_names = tee(revs)
     revs = None  # Don't disturb the tees
                          parse=parse_rev)
     rev_items, rev_names = tee(revs)
     revs = None  # Don't disturb the tees
@@ -643,7 +765,7 @@ def cache_commit(repo, oid):
         name = next(rev_names)
         tip = tip or (name, item)
         entries[name] = item
         name = next(rev_names)
         tip = tip or (name, item)
         entries[name] = item
-    entries['latest'] = FakeLink(meta=default_symlink_mode, target=tip[0])
+    entries[b'latest'] = FakeLink(meta=default_symlink_mode, target=tip[0])
     revlist_key = b'rvl:' + tip[1].coid
     cache_notice(revlist_key, entries)
     return entries
     revlist_key = b'rvl:' + tip[1].coid
     cache_notice(revlist_key, entries)
     return entries
@@ -653,8 +775,8 @@ def revlist_items(repo, oid, names):
 
     # Special case '.' instead of caching the whole history since it's
     # the only way to get the metadata for the commit.
 
     # Special case '.' instead of caching the whole history since it's
     # the only way to get the metadata for the commit.
-    if names and all(x == '.' for x in names):
-        yield '.', _revlist_item_from_oid(repo, oid, True)
+    if names and all(x == b'.' for x in names):
+        yield b'.', _revlist_item_from_oid(repo, oid, True)
         return
 
     # For now, don't worry about the possibility of the contents being
         return
 
     # For now, don't worry about the possibility of the contents being
@@ -670,11 +792,11 @@ def revlist_items(repo, oid, names):
         return
 
     names = frozenset(name for name in names
         return
 
     names = frozenset(name for name in names
-                      if _save_name_rx.match(name) or name in ('.', 'latest'))
+                      if _save_name_rx.match(name) or name in (b'.', b'latest'))
 
 
-    if '.' in names:
-        yield '.', entries['.']
-    for name in (n for n in names if n != '.'):
+    if b'.' in names:
+        yield b'.', entries[b'.']
+    for name in (n for n in names if n != b'.'):
         commit = entries.get(name)
         if commit:
             yield name, commit
         commit = entries.get(name)
         if commit:
             yield name, commit
@@ -684,24 +806,25 @@ def tags_items(repo, names):
 
     def tag_item(oid):
         assert len(oid) == 20
 
     def tag_item(oid):
         assert len(oid) == 20
-        oidx = oid.encode('hex')
+        oidx = hexlify(oid)
         it = repo.cat(oidx)
         _, typ, size = next(it)
         it = repo.cat(oidx)
         _, typ, size = next(it)
-        if typ == 'commit':
+        if typ == b'commit':
             return cache_get_commit_item(oid, need_meta=False) \
             return cache_get_commit_item(oid, need_meta=False) \
-                or _commit_item_from_data(oid, ''.join(it))
+                or _commit_item_from_data(oid, b''.join(it))
         for _ in it: pass
         for _ in it: pass
-        if typ == 'blob':
+        if typ == b'blob':
             return Item(meta=default_file_mode, oid=oid)
             return Item(meta=default_file_mode, oid=oid)
-        elif typ == 'tree':
+        elif typ == b'tree':
             return Item(meta=default_dir_mode, oid=oid)
             return Item(meta=default_dir_mode, oid=oid)
-        raise Exception('unexpected tag type ' + typ + ' for tag ' + name)
+        raise Exception('unexpected tag type ' + typ.decode('ascii')
+                        + ' for tag ' + path_msg(name))
 
     if not names:
 
     if not names:
-        yield '.', _tags
+        yield b'.', _tags
         # We have to pull these all into ram because tag_item calls cat()
         for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
         # We have to pull these all into ram because tag_item calls cat()
         for name, oid in tuple(repo.refs(names, limit_to_tags=True)):
-            assert(name.startswith('refs/tags/'))
+            assert(name.startswith(b'refs/tags/'))
             name = name[10:]
             yield name, tag_item(oid)
         return
             name = name[10:]
             yield name, tag_item(oid)
         return
@@ -711,14 +834,14 @@ def tags_items(repo, names):
         names = frozenset(names)
     remaining = len(names)
     last_name = max(names)
         names = frozenset(names)
     remaining = len(names)
     last_name = max(names)
-    if '.' in names:
-        yield '.', _tags
+    if b'.' in names:
+        yield b'.', _tags
         if remaining == 1:
             return
         remaining -= 1
 
     for name, oid in repo.refs(names, limit_to_tags=True):
         if remaining == 1:
             return
         remaining -= 1
 
     for name, oid in repo.refs(names, limit_to_tags=True):
-        assert(name.startswith('refs/tags/'))
+        assert(name.startswith(b'refs/tags/'))
         name = name[10:]
         if name > last_name:
             return
         name = name[10:]
         if name > last_name:
             return
@@ -756,14 +879,14 @@ def contents(repo, item, names=None, want_meta=True):
     assert S_ISDIR(item_mode(item))
     item_t = type(item)
     if item_t in real_tree_types:
     assert S_ISDIR(item_mode(item))
     item_t = type(item)
     if item_t in real_tree_types:
-        it = repo.cat(item.oid.encode('hex'))
+        it = repo.cat(hexlify(item.oid))
         _, obj_t, size = next(it)
         _, obj_t, size = next(it)
-        data = ''.join(it)
-        if obj_t != 'tree':
+        data = b''.join(it)
+        if obj_t != b'tree':
             for _ in it: pass
             # Note: it shouldn't be possible to see an Item with type
             # 'commit' since a 'commit' should always produce a Commit.
             for _ in it: pass
             # Note: it shouldn't be possible to see an Item with type
             # 'commit' since a 'commit' should always produce a Commit.
-            raise Exception('unexpected git ' + obj_t)
+            raise Exception('unexpected git ' + obj_t.decode('ascii'))
         if want_meta:
             item_gen = tree_items_with_meta(repo, item.oid, data, names)
         else:
         if want_meta:
             item_gen = tree_items_with_meta(repo, item.oid, data, names)
         else:
@@ -782,8 +905,8 @@ def contents(repo, item, names=None, want_meta=True):
 def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
     cache_key = b'res:%d%d%d:%s\0%s' \
                 % (bool(want_meta), bool(follow), repo.id(),
 def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
     cache_key = b'res:%d%d%d:%s\0%s' \
                 % (bool(want_meta), bool(follow), repo.id(),
-                   ('/'.join(x[0] for x in parent) if parent else ''),
-                   '/'.join(path))
+                   (b'/'.join(x[0] for x in parent) if parent else b''),
+                   path)
     resolution = cache_get(cache_key)
     if resolution:
         return resolution
     resolution = cache_get(cache_key)
     if resolution:
         return resolution
@@ -794,7 +917,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
 
     def raise_dir_required_but_not_dir(path, parent, past):
         raise IOError(ENOTDIR,
 
     def raise_dir_required_but_not_dir(path, parent, past):
         raise IOError(ENOTDIR,
-                      "path %r%s resolves to non-directory %r"
+                      "path %s%s resolves to non-directory %r"
                       % (path,
                          ' (relative to %r)' % parent if parent else '',
                          past),
                       % (path,
                          ' (relative to %r)' % parent if parent else '',
                          past),
@@ -817,14 +940,14 @@ def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
         follow = True
     if not future:  # path was effectively '.' or '/'
         if is_absolute:
         follow = True
     if not future:  # path was effectively '.' or '/'
         if is_absolute:
-            return notice_resolution((('', _root),))
+            return notice_resolution(((b'', _root),))
         if parent:
             return notice_resolution(tuple(parent))
         if parent:
             return notice_resolution(tuple(parent))
-        return notice_resolution((('', _root),))
+        return notice_resolution(((b'', _root),))
     if is_absolute:
     if is_absolute:
-        past = [('', _root)]
+        past = [(b'', _root)]
     else:
     else:
-        past = list(parent) if parent else [('', _root)]
+        past = list(parent) if parent else [(b'', _root)]
     hops = 0
     while True:
         if not future:
     hops = 0
     while True:
         if not future:
@@ -832,14 +955,14 @@ def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
                 raise_dir_required_but_not_dir(path, parent, past)
             return notice_resolution(tuple(past))
         segment = future.pop()
                 raise_dir_required_but_not_dir(path, parent, past)
             return notice_resolution(tuple(past))
         segment = future.pop()
-        if segment == '..':
+        if segment == b'..':
             assert len(past) > 0
             if len(past) > 1:  # .. from / is /
                 assert S_ISDIR(item_mode(past[-1][1]))
                 past.pop()
         else:
             parent_name, parent_item = past[-1]
             assert len(past) > 0
             if len(past) > 1:  # .. from / is /
                 assert S_ISDIR(item_mode(past[-1][1]))
                 past.pop()
         else:
             parent_name, parent_item = past[-1]
-            wanted = (segment,) if not want_meta else ('.', segment)
+            wanted = (segment,) if not want_meta else (b'.', segment)
             items = tuple(contents(repo, parent_item, names=wanted,
                                    want_meta=want_meta))
             if not want_meta:
             items = tuple(contents(repo, parent_item, names=wanted,
                                    want_meta=want_meta))
             if not want_meta:
@@ -847,7 +970,7 @@ def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
             else:  # First item will be '.' and have the metadata
                 item = items[1][1] if len(items) == 2 else None
                 dot, dot_item = items[0]
             else:  # First item will be '.' and have the metadata
                 item = items[1][1] if len(items) == 2 else None
                 dot, dot_item = items[0]
-                assert dot == '.'
+                assert dot == b'.'
                 past[-1] = parent_name, parent_item
             if not item:
                 past.append((segment, None),)
                 past[-1] = parent_name, parent_item
             if not item:
                 past.append((segment, None),)
@@ -885,8 +1008,8 @@ def _resolve_path(repo, path, parent=None, want_meta=True, follow=True):
                 is_absolute, _, target_future = _decompose_path(target)
                 if is_absolute:
                     if not target_future:  # path was effectively '/'
                 is_absolute, _, target_future = _decompose_path(target)
                 if is_absolute:
                     if not target_future:  # path was effectively '/'
-                        return notice_resolution((('', _root),))
-                    past = [('', _root)]
+                        return notice_resolution(((b'', _root),))
+                    past = [(b'', _root)]
                     future = target_future
                 else:
                     future.extend(target_future)
                     future = target_future
                 else:
                     future.extend(target_future)
@@ -940,6 +1063,10 @@ def resolve(repo, path, parent=None, want_meta=True, follow=True):
     needed, make a copy via item.meta.copy() and modify that instead.
 
     """
     needed, make a copy via item.meta.copy() and modify that instead.
 
     """
+    if repo.is_remote():
+        # Redirect to the more efficient remote version
+        return repo.resolve(path, parent=parent, want_meta=want_meta,
+                            follow=follow)
     result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
                            follow=follow)
     _, leaf_item = result[-1]
     result = _resolve_path(repo, path, parent=parent, want_meta=want_meta,
                            follow=follow)
     _, leaf_item = result[-1]
@@ -1005,9 +1132,9 @@ def fill_in_metadata_if_dir(repo, item):
 
     """
     if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
 
     """
     if S_ISDIR(item_mode(item)) and not isinstance(item.meta, Metadata):
-        items = tuple(contents(repo, item, ('.',), want_meta=True))
+        items = tuple(contents(repo, item, (b'.',), want_meta=True))
         assert len(items) == 1
         assert len(items) == 1
-        assert items[0][0] == '.'
+        assert items[0][0] == b'.'
         item = items[0][1]
     return item
 
         item = items[0][1]
     return item