Patchwork [3,of,3,v2] repos: introduce '-R readonly:PATH' for doing local operations "read only"

login
register
mail settings
Submitter Mads Kiilerich
Date Oct. 14, 2016, 1:17 a.m.
Message ID <491da143c2f310da732c.1476407850@localhost.localdomain>
Download mbox | patch
Permalink /patch/17078/
State Accepted
Headers show

Comments

Mads Kiilerich - Oct. 14, 2016, 1:17 a.m.
# HG changeset patch
# User Mads Kiilerich <madski@unity3d.com>
# Date 1476402795 -7200
#      Fri Oct 14 01:53:15 2016 +0200
# Node ID 491da143c2f310da732c2e1005e7f8394134a51d
# Parent  24a7ccfb932b134da24e58817991943c8bbd63fa
repos: introduce '-R readonly:PATH' for doing local operations "read only"

When used as 'readonly:.', this is pretty much like if the repository was owned
by another user and the current user didn't have write access to anything in
.hg .

Using this feature will for example allow multiple simultaneous pushes,
pushes without phase changes, and will provide a "safe" way to run commands ...
assuming this and our use of VFS is complete and correct.

The existing "API" for repository types could use some cleanup - it requires
modules with special undefined duck typing. This patch seems to do what is
needed.

The existing VFS class hierarchy has a "readonlyvfs" class, but whatever it is,
it doesn't seem suitable for this use; it doesn't seem to be a reusable class
or mixin.
Yuya Nishihara - Oct. 16, 2016, 1:54 p.m.
On Fri, 14 Oct 2016 03:17:30 +0200, Mads Kiilerich wrote:
> # HG changeset patch
> # User Mads Kiilerich <madski@unity3d.com>
> # Date 1476402795 -7200
> #      Fri Oct 14 01:53:15 2016 +0200
> # Node ID 491da143c2f310da732c2e1005e7f8394134a51d
> # Parent  24a7ccfb932b134da24e58817991943c8bbd63fa
> repos: introduce '-R readonly:PATH' for doing local operations "read only"

> The existing "API" for repository types could use some cleanup - it requires
> modules with special undefined duck typing. This patch seems to do what is
> needed.

I don't know which is better, but you can use a class to provide the API.
That's what schemes.py is doing.

> +# created, not thrown yet
> +readonlyexception = IOError(errno.EACCES, _('this is a "readonly" repository'))
> +
> +# this seems to be a necessary part of the repository type API
> +islocal = localrepo.islocal
> +
> +class readonlyvfs(scmutil.vfs):
> +    """A VFS that only can be called with read modes - writing will fail with
> +    an IO error as if the user didn't have write access"""
> +
> +    def __call__(self, path, mode='r', *args, **kw):
> +        if mode not in ('r', 'rb'):
> +            raise readonlyexception
> +        return super(readonlyvfs, self).__call__(path, mode, *args, **kw)
> +
> +class readonlyrepo(localrepo.localrepository):
> +    """A repository that is local but read only, as if the user didn't have
> +    file system write access."""
> +
> +    def __init__(self, baseui, path=None, create=False):
> +        # we know the "scheme" for path is "readonly" but do not want to extend
> +        # the file/bundle hack in the "url" parser - just strip it here
> +        assert path.startswith('readonly:'), path
> +        path = path[len('readonly:'):]
> +
> +        super(readonlyrepo, self).__init__(baseui, path=path, create=False)
> +
> +        assert self.vfs.__class__ is scmutil.vfs
> +        self.vfs.__class__ = readonlyvfs
> +        assert self.wvfs.__class__ is scmutil.vfs
> +        self.wvfs.__class__ = readonlyvfs

Don't we need to wrap svfs as well?

> +    def lock(self, wait=True):
> +        raise readonlyexception
> +
> +    def wlock(self, wait=True):
> +        raise readonlyexception

Some operations (e.g status) "try" to take a lock to update dirstate or cache
files. In which case, LockUnavailable should be raised.

Patch

diff --git a/mercurial/hg.py b/mercurial/hg.py
--- a/mercurial/hg.py
+++ b/mercurial/hg.py
@@ -31,6 +31,7 @@  from . import (
     merge as mergemod,
     node,
     phases,
+    readonlyrepo,
     repoview,
     scmutil,
     sshpeer,
@@ -112,6 +113,7 @@  schemes = {
     'https': httppeer,
     'ssh': sshpeer,
     'static-http': statichttprepo,
+    'readonly': lambda path: readonlyrepo,
 }
 
 def _peerlookup(path):
diff --git a/mercurial/readonlyrepo.py b/mercurial/readonlyrepo.py
new file mode 100644
--- /dev/null
+++ b/mercurial/readonlyrepo.py
@@ -0,0 +1,57 @@ 
+# readonlyrepo.py - a local repository class for mercurial that can't write
+# or lock the repository
+#
+# This software may be used and distributed according to the terms of the
+# GNU General Public License version 2 or any later version.
+
+from __future__ import absolute_import
+
+import errno
+
+from .i18n import _
+from . import (
+    localrepo,
+    scmutil,
+    util,
+)
+
+# created, not thrown yet
+readonlyexception = IOError(errno.EACCES, _('this is a "readonly" repository'))
+
+# this seems to be a necessary part of the repository type API
+islocal = localrepo.islocal
+
+class readonlyvfs(scmutil.vfs):
+    """A VFS that only can be called with read modes - writing will fail with
+    an IO error as if the user didn't have write access"""
+
+    def __call__(self, path, mode='r', *args, **kw):
+        if mode not in ('r', 'rb'):
+            raise readonlyexception
+        return super(readonlyvfs, self).__call__(path, mode, *args, **kw)
+
+class readonlyrepo(localrepo.localrepository):
+    """A repository that is local but read only, as if the user didn't have
+    file system write access."""
+
+    def __init__(self, baseui, path=None, create=False):
+        # we know the "scheme" for path is "readonly" but do not want to extend
+        # the file/bundle hack in the "url" parser - just strip it here
+        assert path.startswith('readonly:'), path
+        path = path[len('readonly:'):]
+
+        super(readonlyrepo, self).__init__(baseui, path=path, create=False)
+
+        assert self.vfs.__class__ is scmutil.vfs
+        self.vfs.__class__ = readonlyvfs
+        assert self.wvfs.__class__ is scmutil.vfs
+        self.wvfs.__class__ = readonlyvfs
+
+    def lock(self, wait=True):
+        raise readonlyexception
+
+    def wlock(self, wait=True):
+        raise readonlyexception
+
+def instance(ui, path, create):
+    return readonlyrepo(ui, util.urllocalpath(path), create=False)
diff --git a/tests/test-phases-exchange.t b/tests/test-phases-exchange.t
--- a/tests/test-phases-exchange.t
+++ b/tests/test-phases-exchange.t
@@ -1197,25 +1197,27 @@  publish changesets as plain push does
   |
   ~
 
-  $ hg -R Upsilon push Pi -r 7
+  $ hg -R readonly:Upsilon push Pi -r 7
   pushing to Pi
   searching for changes
   no changes found
+  cannot lock source repo, skipping local public phase update
   [1]
   $ hgph Upsilon -r 'min(draft())'
-  o  8 draft a-F - b740e3e5c05d
+  o  2 draft a-C - 54acac6f23ab
   |
   ~
 
-  $ hg -R Upsilon push Pi -r 8
+  $ hg -R readonly:Upsilon push Pi -r 8
   pushing to Pi
   searching for changes
   adding changesets
   adding manifests
   adding file changes
   added 1 changesets with 1 changes to 1 files
+  cannot lock source repo, skipping local public phase update
 
   $ hgph Upsilon -r 'min(draft())'
-  o  9 draft a-G - 3e27b6f1eee1
+  o  2 draft a-C - 54acac6f23ab
   |
   ~