Change:
<ins> --- a/lib/redmine/scm/adapters/mercurial/redminehelper.py +++ b/lib/redmine/scm/adapters/mercurial/redminehelper.py @@ -45,17 +45,20 @@ Output example of rhmanifest:: </repository> </rhmanifest> """ -import re, time, cgi, urllib +import re, time, html, urllib from mercurial import cmdutil, commands, node, error, hg, registrar cmdtable = {} command = registrar.command(cmdtable) if hasattr(registrar, 'command') else cmdutil.command(cmdtable) -_x = cgi.escape -_u = lambda s: cgi.escape(urllib.quote(s)) +_x = lambda s: html.escape(s.decode('utf-8')).encode('utf-8') +_u = lambda s: html.escape(urllib.parse.quote(s)).encode('utf-8') + +def unquoteplus(*args, **kwargs): +    return urllib.parse.unquote_to_bytes(*args, **kwargs).replace(b'+', b' ') def _changectx(repo, rev): -    if isinstance(rev, str): +    if isinstance(rev, bytes): rev = repo.lookup(rev) if hasattr(repo, 'changectx'): return repo.changectx(rev) @@ -70,10 +73,10 @@ def _tip(ui, repo): except TypeError:  # Mercurial < 1.1 return repo.changelog.count() - 1 tipctx = _changectx(repo, tiprev()) -    ui.write('<tip revision="%d" node="%s"/>n' +    ui.write(b'<tip revision="%d" node="%s"/>n' % (tipctx.rev(), _x(node.hex(tipctx.node())))) -_SPECIAL_TAGS = ('tip',) +_SPECIAL_TAGS = (b'tip',) def _tags(ui, repo): # see mercurial/commands.py:tags @@ -84,7 +87,7 @@ def _tags(ui, repo): r = repo.changelog.rev(n) except error.LookupError: continue -        ui.write('<tag revision="%d" node="%s" name="%s"/>n' +        ui.write(b'<tag revision="%d" node="%s" name="%s"/>n' % (r, _x(node.hex(n)), _x(t))) def _branches(ui, repo): @@ -104,136 +107,148 @@ def _branches(ui, repo): return repo.branchheads(branch) def lookup(rev, n): try: -            return repo.lookup(rev) +            return repo.lookup(str(rev).encode('utf-8')) except RuntimeError: return n for t, n, r in sorted(iterbranches(), key=lambda e: e[2], reverse=True): if lookup(r, n) in branchheads(t): -            ui.write('<branch revision="%d" node="%s" name="%s"/>n' +            ui.write(b'<branch revision="%d" node="%s" name="%s"/>n' % (r, _x(node.hex(n)), _x(t))) def _manifest(ui, repo, path, rev): ctx = _changectx(repo, rev) -    ui.write('<manifest revision="%d" path="%s">n' +    ui.write(b'<manifest revision="%d" path="%s">n' % (ctx.rev(), _u(path))) known = set() -    pathprefix = (path.rstrip('/') + '/').lstrip('/') +    pathprefix = (path.decode('utf-8').rstrip('/') + '/').lstrip('/') for f, n in sorted(ctx.manifest().iteritems(), key=lambda e: e[0]): -        if not f.startswith(pathprefix): +        fstr = f.decode('utf-8') +        if not fstr.startswith(pathprefix): continue -        name = re.sub(r'/.*', '/', f[len(pathprefix):]) +        name = re.sub(r'/.*', '/', fstr[len(pathprefix):]) if name in known: continue known.add(name) if name.endswith('/'): -            ui.write('<dir name="%s"/>n' +            ui.write(b'<dir name="%s"/>n' % _x(urllib.quote(name[:-1]))) else: fctx = repo.filectx(f, fileid=n) tm, tzoffset = fctx.date() -            ui.write('<file name="%s" revision="%d" node="%s" ' -                     'time="%d" size="%d"/>n' +            ui.write(b'<file name="%s" revision="%d" node="%s" ' +                     b'time="%d" size="%d"/>n' % (_u(name), fctx.rev(), _x(node.hex(fctx.node())), tm, fctx.size(), )) -    ui.write('</manifest>n') +    ui.write(b'</manifest>n') -@command('rhannotate', -         [('r', 'rev', '', 'revision'), -          ('u', 'user', None, 'list the author (long with -v)'), -          ('n', 'number', None, 'list the revision number (default)'), -          ('c', 'changeset', None, 'list the changeset'), +@command(b'rhannotate', +         [(b'r', b'rev', b'', b'revision'), +          (b'u', b'user', None, b'list the author (long with -v)'), +          (b'n', b'number', None, b'list the revision number (default)'), +          (b'c', b'changeset', None, b'list the changeset'), ], -         'hg rhannotate [-r REV] [-u] [-n] [-c] FILE...') +         b'hg rhannotate [-r REV] [-u] [-n] [-c] FILE...') def rhannotate(ui, repo, *pats, **opts): -    rev = urllib.unquote_plus(opts.pop('rev', None)) +    rev = unquoteplus(opts.pop('rev', b'')) opts['rev'] = rev -    return commands.annotate(ui, repo, *map(urllib.unquote_plus, pats), **opts) +    return commands.annotate(ui, repo, *map(unquoteplus, pats), **opts) -@command('rhcat', -               [('r', 'rev', '', 'revision')], -               'hg rhcat ([-r REV] ...) FILE...') +@command(b'rhcat', +               [(b'r', b'rev', b'', b'revision')], +               b'hg rhcat ([-r REV] ...) FILE...') def rhcat(ui, repo, file1, *pats, **opts): -    rev = urllib.unquote_plus(opts.pop('rev', None)) +    rev = unquoteplus(opts.pop('rev', b'')) opts['rev'] = rev -    return commands.cat(ui, repo, urllib.unquote_plus(file1), *map(urllib.unquote_plus, pats), **opts) +    return commands.cat(ui, repo, unquoteplus(file1), *map(unquoteplus, pats), **opts) -@command('rhdiff', -               [('r', 'rev', [], 'revision'), -                ('c', 'change', '', 'change made by revision')], -               'hg rhdiff ([-c REV] | [-r REV] ...) [FILE]...') +@command(b'rhdiff', +               [(b'r', b'rev', [], b'revision'), +                (b'c', b'change', b'', b'change made by revision')], +               b'hg rhdiff ([-c REV] | [-r REV] ...) [FILE]...') def rhdiff(ui, repo, *pats, **opts): """diff repository (or selected files)""" change = opts.pop('change', None) if change:  # add -c option for Mercurial<1.1 base = _changectx(repo, change).parents()[0].rev() -        opts['rev'] = [str(base), change] +        opts['rev'] = [base, change] opts['nodates'] = True -    return commands.diff(ui, repo, *map(urllib.unquote_plus, pats), **opts) - -@command('rhlog', -                   [ -                    ('r', 'rev', [], 'show the specified revision'), -                    ('b', 'branch', [], -                       'show changesets within the given named branch'), -                    ('l', 'limit', '', -                         'limit number of changes displayed'), -                    ('d', 'date', '', -                         'show revisions matching date spec'), -                    ('u', 'user', [], -                      'revisions committed by user'), -                    ('', 'from', '', -                      ''), -                    ('', 'to', '', -                      ''), -                    ('', 'rhbranch', '', -                      ''), -                    ('', 'template', '', -                       'display with template')], -                   'hg rhlog [OPTION]... [FILE]') +    return commands.diff(ui, repo, *map(unquoteplus, pats), **opts) + +@command(b'rhlog', +                  [ +                    (b'r', b'rev', [], b'show the specified revision'), +                    (b'b', b'branch', [], +                       b'show changesets within the given named branch'), +                    (b'l', b'limit', b'', +                         b'limit number of changes displayed'), +                    (b'd', b'date', b'', +                         b'show revisions matching date spec'), +                    (b'u', b'user', [], +                      b'revisions committed by user'), +                    (b'', b'from', b'', +                      b''), +                    (b'', b'to', b'', +                      b''), +                    (b'', b'rhbranch', b'', +                      b''), +                    (b'', b'template', b'', +                       b'display with template')], +                   b'hg rhlog [OPTION]... [FILE]') + def rhlog(ui, repo, *pats, **opts): rev      = opts.pop('rev') bra0     = opts.pop('branch') -    from_rev = urllib.unquote_plus(opts.pop('from', None)) -    to_rev   = urllib.unquote_plus(opts.pop('to'  , None)) -    bra      = urllib.unquote_plus(opts.pop('rhbranch', None)) -    from_rev = from_rev.replace('"', '"') -    to_rev   = to_rev.replace('"', '"') -    if hg.util.version() >= '1.6': -      opts['rev'] = ['"%s":"%s"' % (from_rev, to_rev)] +    from_rev = unquoteplus(opts.pop('from', b'')) +    to_rev   = unquoteplus(opts.pop('to'  , b'')) +    bra      = unquoteplus(opts.pop('rhbranch', b'')) +    from_rev = from_rev.replace(b'"', b'"') +    to_rev   = to_rev.replace(b'"', b'"') +    if (from_rev != b'') or (to_rev != b''): +        if from_rev != b'': +            quotefrom = b'"%s"' % (from_rev) +        else: +            quotefrom = from_rev +        if to_rev != b'': +            quoteto = b'"%s"' % (to_rev) +        else: +            quoteto = to_rev +        opts['rev'] = [b'%s:%s' % (quotefrom, quoteto)] else: -      opts['rev'] = ['%s:%s' % (from_rev, to_rev)] -    opts['branch'] = [bra] -    return commands.log(ui, repo, *map(urllib.unquote_plus, pats), **opts) - -@command('rhmanifest', -                   [('r', 'rev', '', 'show the specified revision')], -                   'hg rhmanifest [-r REV] [PATH]') -def rhmanifest(ui, repo, path='', **opts): +        opts['rev'] = rev +    if (bra != b''): +        opts['branch'] = [bra] +    return commands.log(ui, repo, *map(unquoteplus, pats), **opts) + + +@command(b'rhmanifest', +                   [(b'r', b'rev', b'', b'show the specified revision')], +                   b'hg rhmanifest -r REV [PATH]') +def rhmanifest(ui, repo, path=b'', **opts): """output the sub-manifest of the specified directory""" -    ui.write('<?xml version="1.0"?>n') -    ui.write('<rhmanifest>n') -    ui.write('<repository root="%s">n' % _u(repo.root)) +    ui.write(b'<?xml version="1.0"?>n') +    ui.write(b'<rhmanifest>n') +    ui.write(b'<repository root="%s">n' % _u(repo.root)) try: -        _manifest(ui, repo, urllib.unquote_plus(path), urllib.unquote_plus(opts.get('rev'))) +        _manifest(ui, repo, unquoteplus(path), unquoteplus(opts.get('rev'))) finally: -        ui.write('</repository>n') -        ui.write('</rhmanifest>n') +        ui.write(b'</repository>n') +        ui.write(b'</rhmanifest>n') -@command('rhsummary',[], 'hg rhsummary') +@command(b'rhsummary',[], b'hg rhsummary') def rhsummary(ui, repo, **opts): """output the summary of the repository""" -    ui.write('<?xml version="1.0"?>n') -    ui.write('<rhsummary>n') -    ui.write('<repository root="%s">n' % _u(repo.root)) +    ui.write(b'<?xml version="1.0"?>n') +    ui.write(b'<rhsummary>n') +    ui.write(b'<repository root="%s">n' % _u(repo.root)) try: _tip(ui, repo) _tags(ui, repo) _branches(ui, repo) # TODO: bookmarks in core (Mercurial>=1.8) finally: -        ui.write('</repository>n') -        ui.write('</rhsummary>n') +        ui.write(b'</repository>n') +        ui.write(b'</rhsummary>n') </ins>

Discussion:
Describe the bug Browsing a hg (mercurial) repository in a redmine project does not work. No repo functionality works when hg is used. To Reproduce Steps to reproduce the behavior: use of the redmine module a local hg repo to which redmine has access. redmine, go to the project settings page and add the local hg repo the repo name, or click on the "repository" tab of the project Now you get to see a 404 page. Expected behavior Normally you should be able to browse the repository. Logs This issue happens both in the current nixos release and in the unstable, but with different errors: In release 20.03 In unstable Additional context The python hg script that is shipped with redmine () seems to be incompatible with the python version is being used when it is called. Possible related upstream bug: Notify maintainers
I've never used so wouldn't know where to start. Can you provide some config that I can reproduce this with and maybe something will occur to me?
Not sure what I can give in terms of config. In order to reproduce you basically do the same as you would with git: And then add it as a repository in the redmine project config, like so: ! EDIT: Not sure if the path should or shoudn't include the at the end, but I didn't get it to work either way. If you then click on the repo link "hg", or on "repository" tab of the project you get: ! I then checked what the error log was with: The following archive has the state directory + also the repo directory as described above + , a database dump of the redmine mysql database (login: admin, password: testtest) To be honest I'm not sure if this can be solved in NixOS at all. We might just have to wait until compatibility with python3 is improved upstream.
Sounds like an upstream bug - have you tried reporting it upstream?
I think it's already reported: But I'll add a comment there linking back here... The patch posted there might solve the issue for us.
I created a draft PR for this, which I verified fixes this issue.

Though I don't know if we really want to carry around a patch ourselves until upstream fixes things... The current mercurial <-redmine situation can't really get any worse, and I don't think this patch can break anything else. But it might also be worth it to wait for a proper fix from upstream.
I think's fine to carry a patch until upstream fixes things if these two things are met: upstream is already aware of the patch - done our patch contains reference to the upstream bug report - not done, left you a suggestion in the PR - see