diff --git a/contrib/hg-ssh b/contrib/hg-ssh --- a/contrib/hg-ssh +++ b/contrib/hg-ssh @@ -24,6 +24,9 @@ command="cd path/to/my/repositories && h You can use pattern matching of your normal shell, e.g.: command="cd repos && hg-ssh user/thomas/* projects/{mercurial,foo}" + +You can also add a --read-only flag to allow read-only access to a key, e.g.: +command="hg-ssh --read-only repos/*" """ # enable importing on demand to reduce startup time @@ -35,9 +38,17 @@ import sys, os, shlex def main(): cwd = os.getcwd() + readonly = False + args = sys.argv[1:] + while len(args): + if args[0] == '--read-only': + readonly = True + args.pop(0) + else: + break allowed_paths = [os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) - for path in sys.argv[1:]] + for path in args] orig_cmd = os.getenv('SSH_ORIGINAL_COMMAND', '?') try: cmdargv = shlex.split(orig_cmd) @@ -49,9 +60,15 @@ def main(): path = cmdargv[2] repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) if repo in allowed_paths: - dispatch.dispatch(dispatch.request(['-R', repo, - 'serve', - '--stdio'])) + cmd = ['-R', repo, 'serve', '--stdio'] + if readonly: + cmd += [ + '--config', + 'hooks.prechangegroup.hg-ssh=python:__main__.rejectpush', + '--config', + 'hooks.prepushkey.hg-ssh=python:__main__.rejectpush' + ] + dispatch.dispatch(dispatch.request(cmd)) else: sys.stderr.write('Illegal repository "%s"\n' % repo) sys.exit(255) @@ -59,5 +76,11 @@ def main(): sys.stderr.write('Illegal command "%s"\n' % orig_cmd) sys.exit(255) +def rejectpush(ui, **kwargs): + ui.warn("Permission denied\n") + # mercurial hooks use unix process conventions for hook return values + # so a truthy return means failure + return True + if __name__ == '__main__': main() diff --git a/tests/test-ssh.t b/tests/test-ssh.t --- a/tests/test-ssh.t +++ b/tests/test-ssh.t @@ -308,6 +308,41 @@ parameters: Illegal command "'hg' -R 'a'repo' serve --stdio": No closing quotation [255] +Test hg-ssh in read-only mode: + + $ cat > ssh.sh << EOF + > userhost="\$1" + > SSH_ORIGINAL_COMMAND="\$2" + > export SSH_ORIGINAL_COMMAND + > PYTHONPATH="$PYTHONPATH" + > export PYTHONPATH + > python "$TESTDIR/../contrib/hg-ssh" --read-only "$TESTTMP/remote" + > EOF + + $ hg clone --ssh "sh ssh.sh" "ssh://user@dummy/$TESTTMP/remote" read-only-local + requesting all changes + adding changesets + adding manifests + adding file changes + added 4 changesets with 5 changes to 4 files (+1 heads) + updating to branch default + 3 files updated, 0 files merged, 0 files removed, 0 files unresolved + + $ cd read-only-local + $ echo "baz" > bar + $ hg ci -A -m "unpushable commit" bar + $ hg push --ssh "sh ../ssh.sh" + pushing to ssh://user@dummy/$TESTTMP/remote + searching for changes + remote: Permission denied + remote: abort: prechangegroup.hg-ssh hook failed + remote: Permission denied + remote: abort: prepushkey.hg-ssh hook failed + abort: unexpected response: empty string + [255] + + $ cd .. + $ cat dummylog Got arguments 1:user@dummy 2:hg -R nonexistent serve --stdio Got arguments 1:user@dummy 2:hg -R /$TESTTMP/nonexistent serve --stdio