changeset 8568:a04d6926d694

merge stable
author Thomas De Schampheleire <thomas.de_schampheleire@nokia.com>
date Wed, 17 Jun 2020 21:52:16 +0200
parents cc909683116c (diff) 0bca9e828db2 (current diff)
children 87de82eb7cb0
files development.ini docs/overview.rst kallithea/lib/paster_commands/template.ini.mako kallithea/lib/vcs/backends/hg/repository.py
diffstat 48 files changed, 645 insertions(+), 463 deletions(-) [+]
line wrap: on
line diff
--- a/dev_requirements.txt	Thu Jun 11 20:50:26 2020 +0200
+++ b/dev_requirements.txt	Wed Jun 17 21:52:16 2020 +0200
@@ -1,9 +1,9 @@
-pytest >= 4.6.6, < 5.4
+pytest >= 4.6.6, < 5.5
 pytest-sugar >= 0.9.2, < 0.10
 pytest-benchmark >= 3.2.2, < 3.3
 pytest-localserver >= 0.5.0, < 0.6
 mock >= 3.0.0, < 4.1
-Sphinx >= 1.8.0, < 2.4
+Sphinx >= 1.8.0, < 3.1
 WebTest >= 2.0.6, < 2.1
 isort == 4.3.21
-pyflakes == 2.1.1
+pyflakes == 2.2.0
--- a/development.ini	Thu Jun 11 20:50:26 2020 +0200
+++ b/development.ini	Wed Jun 17 21:52:16 2020 +0200
@@ -67,11 +67,11 @@
 host = 0.0.0.0
 port = 5000
 
-## WAITRESS ##
+## Gearbox serve uses the Waitress web server ##
 use = egg:waitress#main
-## number of worker threads
+## avoid multi threading
 threads = 1
-## MAX BODY SIZE 100GB
+## allow push of repos bigger than the default of 1 GB
 max_request_body_size = 107374182400
 ## use poll instead of select, fixes fd limits, may not work on old
 ## windows systems.
@@ -359,10 +359,10 @@
 ##      DB CONFIG      ##
 #########################
 
-## SQLITE [default]
 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
-
-## see sqlalchemy docs for other backends
+#sqlalchemy.url = postgresql://user:pass@localhost/kallithea
+#sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
+## Note: the mysql:// prefix should also be used for MariaDB
 
 sqlalchemy.pool_recycle = 3600
 
--- a/kallithea/__init__.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/__init__.py	Wed Jun 17 21:52:16 2020 +0200
@@ -34,7 +34,7 @@
 if sys.version_info < (3, 6):
     raise Exception('Kallithea requires python 3.6 or later')
 
-VERSION = (0, 6, 1)
+VERSION = (0, 6, 99)
 BACKENDS = {
     'hg': 'Mercurial repository',
     'git': 'Git repository',
--- a/kallithea/bin/kallithea_cli_base.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/bin/kallithea_cli_base.py	Wed Jun 17 21:52:16 2020 +0200
@@ -23,7 +23,7 @@
 import paste.deploy
 
 import kallithea
-import kallithea.config.middleware
+import kallithea.config.application
 
 
 # kallithea_cli is usually invoked through the 'kallithea-cli' wrapper script
@@ -77,7 +77,7 @@
                 logging.config.fileConfig(cp,
                     {'__file__': path_to_ini_file, 'here': os.path.dirname(path_to_ini_file)})
                 if config_file_initialize_app:
-                    kallithea.config.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
+                    kallithea.config.application.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
                 return annotated(*args, **kwargs)
             return cli_command(runtime_wrapper)
         return annotator
--- a/kallithea/bin/kallithea_cli_db.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/bin/kallithea_cli_db.py	Wed Jun 17 21:52:16 2020 +0200
@@ -67,7 +67,7 @@
     Session().commit()
 
     # initial repository scan
-    kallithea.config.middleware.make_app(
+    kallithea.config.application.make_app(
             kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
     added, _ = kallithea.lib.utils.repo2db_mapper(kallithea.model.scm.ScmModel().repo_scan())
     if added:
--- a/kallithea/bin/kallithea_cli_ssh.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/bin/kallithea_cli_ssh.py	Wed Jun 17 21:52:16 2020 +0200
@@ -21,7 +21,7 @@
 
 import kallithea
 import kallithea.bin.kallithea_cli_base as cli_base
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.lib.vcs.backends.git.ssh import GitSshHandler
 from kallithea.lib.vcs.backends.hg.ssh import MercurialSshHandler
 from kallithea.model.ssh_key import SshKeyModel, SshKeyModelException
@@ -40,8 +40,7 @@
     protocol access. The access will be granted as the specified user ID, and
     logged as using the specified key ID.
     """
-    ssh_enabled = kallithea.CONFIG.get('ssh_enabled', False)
-    if not str2bool(ssh_enabled):
+    if not asbool(kallithea.CONFIG.get('ssh_enabled', False)):
         sys.stderr.write("SSH access is disabled.\n")
         return sys.exit(1)
 
--- a/kallithea/config/app_cfg.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/config/app_cfg.py	Wed Jun 17 21:52:16 2020 +0200
@@ -28,82 +28,57 @@
 from alembic.migration import MigrationContext
 from alembic.script.base import ScriptDirectory
 from sqlalchemy import create_engine
-from tg.configuration import AppConfig
-from tg.support.converters import asbool
+from tg import FullStackApplicationConfigurator
 
 import kallithea.lib.locale
 import kallithea.model.base
 import kallithea.model.meta
 from kallithea.lib import celerypylons
-from kallithea.lib.middleware.https_fixup import HttpsFixup
-from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl
-from kallithea.lib.middleware.simplegit import SimpleGit
-from kallithea.lib.middleware.simplehg import SimpleHg
-from kallithea.lib.middleware.wrapper import RequestWrapper
 from kallithea.lib.utils import check_git_version, load_rcextensions, set_app_settings, set_indexer_config, set_vcs_config
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.model import db
 
 
 log = logging.getLogger(__name__)
 
 
-class KallitheaAppConfig(AppConfig):
-    # Note: AppConfig has a misleading name, as it's not the application
-    # configuration, but the application configurator. The AppConfig values are
-    # used as a template to create the actual configuration, which might
-    # overwrite or extend the one provided by the configurator template.
+base_config = FullStackApplicationConfigurator()
 
-    # To make it clear, AppConfig creates the config and sets into it the same
-    # values that AppConfig itself has. Then the values from the config file and
-    # gearbox options are loaded and merged into the configuration. Then an
-    # after_init_config(conf) method of AppConfig is called for any change that
-    # might depend on options provided by configuration files.
+base_config.update_blueprint({
+    'package': kallithea,
 
-    def __init__(self):
-        super(KallitheaAppConfig, self).__init__()
-
-        self['package'] = kallithea
+    # Rendering Engines Configuration
+    'renderers': [
+        'json',
+        'mako',
+    ],
+    'default_renderer': 'mako',
+    'use_dotted_templatenames': False,
 
-        self['prefer_toscawidgets2'] = False
-        self['use_toscawidgets'] = False
-
-        self['renderers'] = []
-
-        # Enable json in expose
-        self['renderers'].append('json')
+    # Configure Sessions, store data as JSON to avoid pickle security issues
+    'session.enabled': True,
+    'session.data_serializer': 'json',
 
-        # Configure template rendering
-        self['renderers'].append('mako')
-        self['default_renderer'] = 'mako'
-        self['use_dotted_templatenames'] = False
+    # Configure the base SQLALchemy Setup
+    'use_sqlalchemy': True,
+    'model': kallithea.model.base,
+    'DBSession': kallithea.model.meta.Session,
 
-        # Configure Sessions, store data as JSON to avoid pickle security issues
-        self['session.enabled'] = True
-        self['session.data_serializer'] = 'json'
-
-        # Configure the base SQLALchemy Setup
-        self['use_sqlalchemy'] = True
-        self['model'] = kallithea.model.base
-        self['DBSession'] = kallithea.model.meta.Session
+    # Configure App without an authentication backend.
+    'auth_backend': None,
 
-        # Configure App without an authentication backend.
-        self['auth_backend'] = None
-
-        # Use custom error page for these errors. By default, Turbogears2 does not add
-        # 400 in this list.
-        # Explicitly listing all is considered more robust than appending to defaults,
-        # in light of possible future framework changes.
-        self['errorpage.status_codes'] = [400, 401, 403, 404]
+    # Use custom error page for these errors. By default, Turbogears2 does not add
+    # 400 in this list.
+    # Explicitly listing all is considered more robust than appending to defaults,
+    # in light of possible future framework changes.
+    'errorpage.status_codes': [400, 401, 403, 404],
 
-        # Disable transaction manager -- currently Kallithea takes care of transactions itself
-        self['tm.enabled'] = False
+    # Disable transaction manager -- currently Kallithea takes care of transactions itself
+    'tm.enabled': False,
 
-        # Set the default i18n source language so TG doesn't search beyond 'en' in Accept-Language.
-        self['i18n.lang'] = 'en'
-
-
-base_config = KallitheaAppConfig()
+    # Set the default i18n source language so TG doesn't search beyond 'en' in Accept-Language.
+    'i18n.lang': 'en',
+})
 
 # DebugBar, a debug toolbar for TurboGears2.
 # (https://github.com/TurboGears/tgext.debugbar)
@@ -160,7 +135,7 @@
     # store some globals into kallithea
     kallithea.DEFAULT_USER_ID = db.User.get_default_user().user_id
 
-    if str2bool(config.get('use_celery')):
+    if asbool(config.get('use_celery')):
         kallithea.CELERY_APP = celerypylons.make_app()
     kallithea.CONFIG = config
 
@@ -188,27 +163,3 @@
 
 
 tg.hooks.register('configure_new_app', setup_configuration)
-
-
-def setup_application(app):
-    config = app.config
-
-    # we want our low level middleware to get to the request ASAP. We don't
-    # need any stack middleware in them - especially no StatusCodeRedirect buffering
-    app = SimpleHg(app, config)
-    app = SimpleGit(app, config)
-
-    # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
-    if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
-        app = HttpsFixup(app, config)
-
-    app = PermanentRepoUrl(app, config)
-
-    # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead
-    if str2bool(config.get('use_wsgi_wrapper')):
-        app = RequestWrapper(app, config)
-
-    return app
-
-
-tg.hooks.register('before_config', setup_application)
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/kallithea/config/application.py	Wed Jun 17 21:52:16 2020 +0200
@@ -0,0 +1,68 @@
+# -*- coding: utf-8 -*-
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+"""WSGI middleware initialization for the Kallithea application."""
+
+from kallithea.config.app_cfg import base_config
+from kallithea.lib.middleware.https_fixup import HttpsFixup
+from kallithea.lib.middleware.permanent_repo_url import PermanentRepoUrl
+from kallithea.lib.middleware.simplegit import SimpleGit
+from kallithea.lib.middleware.simplehg import SimpleHg
+from kallithea.lib.middleware.wrapper import RequestWrapper
+from kallithea.lib.utils2 import asbool
+
+
+__all__ = ['make_app']
+
+
+def wrap_app(app):
+    """Wrap the TG WSGI application in Kallithea middleware"""
+    config = app.config
+
+    # we want our low level middleware to get to the request ASAP. We don't
+    # need any stack middleware in them - especially no StatusCodeRedirect buffering
+    app = SimpleHg(app, config)
+    app = SimpleGit(app, config)
+
+    # Enable https redirects based on HTTP_X_URL_SCHEME set by proxy
+    if any(asbool(config.get(x)) for x in ['https_fixup', 'force_https', 'use_htsts']):
+        app = HttpsFixup(app, config)
+
+    app = PermanentRepoUrl(app, config)
+
+    # Optional and undocumented wrapper - gives more verbose request/response logging, but has a slight overhead
+    if asbool(config.get('use_wsgi_wrapper')):
+        app = RequestWrapper(app, config)
+
+    return app
+
+
+def make_app(global_conf, **app_conf):
+    """
+    Set up Kallithea with the settings found in the PasteDeploy configuration
+    file used.
+
+    :param global_conf: The global settings for Kallithea (those
+        defined under the ``[DEFAULT]`` section).
+    :return: The Kallithea application with all the relevant middleware
+        loaded.
+
+    This is the PasteDeploy factory for the Kallithea application.
+
+    ``app_conf`` contains all the application-specific settings (those defined
+    under ``[app:main]``.
+    """
+    assert app_conf.get('sqlalchemy.url')  # must be called with a Kallithea .ini file, which for example must have this config option
+    assert global_conf.get('here') and global_conf.get('__file__')  # app config should be initialized the paste way ...
+
+    return base_config.make_wsgi_app(global_conf, app_conf, wrap_app=wrap_app)
--- a/kallithea/config/environment.py	Thu Jun 11 20:50:26 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-# -*- coding: utf-8 -*-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""WSGI environment setup for Kallithea."""
-
-from kallithea.config.app_cfg import base_config
-
-
-__all__ = ['load_environment']
-
-# Use base_config to setup the environment loader function
-load_environment = base_config.make_load_environment()
--- a/kallithea/config/middleware.py	Thu Jun 11 20:50:26 2020 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,47 +0,0 @@
-# -*- coding: utf-8 -*-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-"""WSGI middleware initialization for the Kallithea application."""
-
-from kallithea.config.app_cfg import base_config
-from kallithea.config.environment import load_environment
-
-
-__all__ = ['make_app']
-
-# Use base_config to setup the necessary PasteDeploy application factory.
-# make_base_app will wrap the TurboGears2 app with all the middleware it needs.
-make_base_app = base_config.setup_tg_wsgi_app(load_environment)
-
-
-def make_app(global_conf, full_stack=True, **app_conf):
-    """
-    Set up Kallithea with the settings found in the PasteDeploy configuration
-    file used.
-
-    :param global_conf: The global settings for Kallithea (those
-        defined under the ``[DEFAULT]`` section).
-    :type global_conf: dict
-    :param full_stack: Should the whole TurboGears2 stack be set up?
-    :type full_stack: str or bool
-    :return: The Kallithea application with all the relevant middleware
-        loaded.
-
-    This is the PasteDeploy factory for the Kallithea application.
-
-    ``app_conf`` contains all the application-specific settings (those defined
-    under ``[app:main]``.
-    """
-    assert app_conf.get('sqlalchemy.url')  # must be called with a Kallithea .ini file, which for example must have this config option
-    assert global_conf.get('here') and global_conf.get('__file__')  # app config should be initialized the paste way ...
-    return make_base_app(global_conf, full_stack=full_stack, **app_conf)
--- a/kallithea/controllers/admin/permissions.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/admin/permissions.py	Wed Jun 17 21:52:16 2020 +0200
@@ -61,18 +61,22 @@
         super(PermissionsController, self)._before(*args, **kwargs)
 
     def __load_data(self):
+        # Permissions for the Default user on new repositories
         c.repo_perms_choices = [('repository.none', _('None'),),
                                    ('repository.read', _('Read'),),
                                    ('repository.write', _('Write'),),
                                    ('repository.admin', _('Admin'),)]
+        # Permissions for the Default user on new repository groups
         c.group_perms_choices = [('group.none', _('None'),),
                                  ('group.read', _('Read'),),
                                  ('group.write', _('Write'),),
                                  ('group.admin', _('Admin'),)]
+        # Permissions for the Default user on new user groups
         c.user_group_perms_choices = [('usergroup.none', _('None'),),
                                       ('usergroup.read', _('Read'),),
                                       ('usergroup.write', _('Write'),),
                                       ('usergroup.admin', _('Admin'),)]
+        # Registration - allow new Users to create an account
         c.register_choices = [
             ('hg.register.none',
                 _('Disabled')),
@@ -80,26 +84,18 @@
                 _('Allowed with manual account activation')),
             ('hg.register.auto_activate',
                 _('Allowed with automatic account activation')), ]
-
+        # External auth account activation
         c.extern_activate_choices = [
             ('hg.extern_activate.manual', _('Manual activation of external account')),
             ('hg.extern_activate.auto', _('Automatic activation of external account')),
         ]
-
+        # Top level repository creation
         c.repo_create_choices = [('hg.create.none', _('Disabled')),
                                  ('hg.create.repository', _('Enabled'))]
-
-        c.repo_create_on_write_choices = [
-            ('hg.create.write_on_repogroup.true', _('Enabled')),
-            ('hg.create.write_on_repogroup.false', _('Disabled')),
-        ]
-
+        # User group creation
         c.user_group_create_choices = [('hg.usergroup.create.false', _('Disabled')),
                                        ('hg.usergroup.create.true', _('Enabled'))]
-
-        c.repo_group_create_choices = [('hg.repogroup.create.false', _('Disabled')),
-                                       ('hg.repogroup.create.true', _('Enabled'))]
-
+        # Repository forking:
         c.fork_choices = [('hg.fork.none', _('Disabled')),
                           ('hg.fork.repository', _('Enabled'))]
 
@@ -112,7 +108,6 @@
                 [x[0] for x in c.group_perms_choices],
                 [x[0] for x in c.user_group_perms_choices],
                 [x[0] for x in c.repo_create_choices],
-                [x[0] for x in c.repo_create_on_write_choices],
                 [x[0] for x in c.repo_group_create_choices],
                 [x[0] for x in c.user_group_create_choices],
                 [x[0] for x in c.fork_choices],
@@ -157,15 +152,9 @@
             if p.permission.permission_name.startswith('usergroup.'):
                 defaults['default_user_group_perm'] = p.permission.permission_name
 
-            if p.permission.permission_name.startswith('hg.create.write_on_repogroup.'):
-                defaults['create_on_write'] = p.permission.permission_name
-
             elif p.permission.permission_name.startswith('hg.create.'):
                 defaults['default_repo_create'] = p.permission.permission_name
 
-            if p.permission.permission_name.startswith('hg.repogroup.'):
-                defaults['default_repo_group_create'] = p.permission.permission_name
-
             if p.permission.permission_name.startswith('hg.usergroup.'):
                 defaults['default_user_group_create'] = p.permission.permission_name
 
--- a/kallithea/controllers/admin/repo_groups.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/admin/repo_groups.py	Wed Jun 17 21:52:16 2020 +0200
@@ -63,7 +63,7 @@
         exclude is used for not moving group to itself TODO: also exclude descendants
         Note: only admin can create top level groups
         """
-        repo_groups = AvailableRepoGroupChoices([], 'admin', extras)
+        repo_groups = AvailableRepoGroupChoices('admin', extras)
         exclude_group_ids = set(rg.group_id for rg in exclude)
         c.repo_groups = [rg for rg in repo_groups
                          if rg[0] not in exclude_group_ids]
--- a/kallithea/controllers/admin/repos.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/admin/repos.py	Wed Jun 17 21:52:16 2020 +0200
@@ -39,7 +39,7 @@
 import kallithea
 from kallithea.config.routing import url
 from kallithea.lib import helpers as h
-from kallithea.lib.auth import HasPermissionAny, HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous
+from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired, NotAnonymous
 from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.exceptions import AttachedForksError
 from kallithea.lib.utils import action_logger
@@ -76,14 +76,9 @@
         return repo_obj
 
     def __load_defaults(self, repo=None):
-        top_perms = ['hg.create.repository']
-        if HasPermissionAny('hg.create.write_on_repogroup.true')():
-            repo_group_perm_level = 'write'
-        else:
-            repo_group_perm_level = 'admin'
         extras = [] if repo is None else [repo.group]
 
-        c.repo_groups = AvailableRepoGroupChoices(top_perms, repo_group_perm_level, extras)
+        c.repo_groups = AvailableRepoGroupChoices('write', extras)
 
         c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs(repo)
 
--- a/kallithea/controllers/feed.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/feed.py	Wed Jun 17 21:52:16 2020 +0200
@@ -39,7 +39,7 @@
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseRepoController
 from kallithea.lib.diffs import DiffProcessor
-from kallithea.lib.utils2 import safe_int, safe_str, str2bool
+from kallithea.lib.utils2 import asbool, safe_int, safe_str
 
 
 log = logging.getLogger(__name__)
@@ -92,7 +92,7 @@
         desc_msg.append(h.urlify_text(cs.message))
         desc_msg.append('\n')
         desc_msg.extend(changes)
-        if str2bool(CONFIG.get('rss_include_diff', False)):
+        if asbool(CONFIG.get('rss_include_diff', False)):
             desc_msg.append('\n\n')
             desc_msg.append(safe_str(raw_diff))
         desc_msg.append('</pre>')
--- a/kallithea/controllers/files.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/files.py	Wed Jun 17 21:52:16 2020 +0200
@@ -46,7 +46,7 @@
 from kallithea.lib.base import BaseRepoController, jsonify, render
 from kallithea.lib.exceptions import NonRelativePathError
 from kallithea.lib.utils import action_logger
-from kallithea.lib.utils2 import convert_line_endings, detect_mode, safe_int, safe_str, str2bool
+from kallithea.lib.utils2 import asbool, convert_line_endings, detect_mode, safe_int, safe_str
 from kallithea.lib.vcs.backends.base import EmptyChangeset
 from kallithea.lib.vcs.conf import settings
 from kallithea.lib.vcs.exceptions import (ChangesetDoesNotExistError, ChangesetError, EmptyRepositoryError, ImproperArchiveTypeError, NodeAlreadyExistsError,
@@ -577,7 +577,7 @@
         # to reduce JS and callbacks
 
         if request.GET.get('show_rev'):
-            if str2bool(request.GET.get('annotate', 'False')):
+            if asbool(request.GET.get('annotate', 'False')):
                 _url = url('files_annotate_home', repo_name=c.repo_name,
                            revision=diff1, f_path=c.f_path)
             else:
--- a/kallithea/controllers/forks.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/controllers/forks.py	Wed Jun 17 21:52:16 2020 +0200
@@ -38,7 +38,7 @@
 import kallithea
 import kallithea.lib.helpers as h
 from kallithea.config.routing import url
-from kallithea.lib.auth import HasPermissionAny, HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired
+from kallithea.lib.auth import HasPermissionAnyDecorator, HasRepoPermissionLevel, HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.page import Page
 from kallithea.lib.utils2 import safe_int
@@ -54,11 +54,7 @@
 class ForksController(BaseRepoController):
 
     def __load_defaults(self):
-        if HasPermissionAny('hg.create.write_on_repogroup.true')():
-            repo_group_perm_level = 'write'
-        else:
-            repo_group_perm_level = 'admin'
-        c.repo_groups = AvailableRepoGroupChoices(['hg.create.repository'], repo_group_perm_level)
+        c.repo_groups = AvailableRepoGroupChoices('write')
 
         c.landing_revs_choices, c.landing_revs = ScmModel().get_repo_landing_revs()
 
--- a/kallithea/lib/auth.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/auth.py	Wed Jun 17 21:52:16 2020 +0200
@@ -149,7 +149,6 @@
         # based on default permissions, just set everything to admin
         #==================================================================
         permissions[GLOBAL].add('hg.admin')
-        permissions[GLOBAL].add('hg.create.write_on_repogroup.true')
 
         # repositories
         for perm in default_repo_perms:
@@ -242,7 +241,7 @@
 
     # for each kind of global permissions, only keep the one with heighest weight
     kind_max_perm = {}
-    for perm in sorted(permissions[GLOBAL], key=lambda n: PERM_WEIGHTS[n]):
+    for perm in sorted(permissions[GLOBAL], key=lambda n: PERM_WEIGHTS.get(n, -1)):
         kind = perm.rsplit('.', 1)[0]
         kind_max_perm[kind] = perm
     permissions[GLOBAL] = set(kind_max_perm.values())
--- a/kallithea/lib/auth_modules/__init__.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/auth_modules/__init__.py	Wed Jun 17 21:52:16 2020 +0200
@@ -21,7 +21,7 @@
 
 from kallithea.lib.auth import AuthUser, PasswordGenerator
 from kallithea.lib.compat import hybrid_property
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.model.db import Setting, User
 from kallithea.model.meta import Session
 from kallithea.model.user import UserModel
@@ -350,7 +350,7 @@
             plugin_settings[v["name"]] = setting.app_settings_value if setting else None
         log.debug('Settings for auth plugin %s: %s', plugin_name, plugin_settings)
 
-        if not str2bool(plugin_settings["enabled"]):
+        if not asbool(plugin_settings["enabled"]):
             log.info("Authentication plugin %s is disabled, skipping for %s",
                      module, username)
             continue
--- a/kallithea/lib/auth_modules/auth_container.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_container.py	Wed Jun 17 21:52:16 2020 +0200
@@ -29,7 +29,7 @@
 
 from kallithea.lib import auth_modules
 from kallithea.lib.compat import hybrid_property
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.model.db import Setting
 
 
@@ -131,7 +131,7 @@
             username = environ.get(header)
             log.debug('extracted %s:%s', header, username)
 
-        if username and str2bool(settings.get('clean_username')):
+        if username and asbool(settings.get('clean_username')):
             log.debug('Received username %s from container', username)
             username = self._clean_username(username)
             log.debug('New cleanup user is: %s', username)
--- a/kallithea/lib/base.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/base.py	Wed Jun 17 21:52:16 2020 +0200
@@ -49,7 +49,7 @@
 from kallithea.lib.auth import AuthUser, HasPermissionAnyMiddleware
 from kallithea.lib.exceptions import UserCreationError
 from kallithea.lib.utils import get_repo_slug, is_valid_repo
-from kallithea.lib.utils2 import AttributeDict, ascii_bytes, safe_int, safe_str, set_hook_environment, str2bool
+from kallithea.lib.utils2 import AttributeDict, asbool, ascii_bytes, safe_int, safe_str, set_hook_environment
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError, EmptyRepositoryError, RepositoryError
 from kallithea.model import meta
 from kallithea.model.db import PullRequest, Repository, Setting, User
@@ -375,14 +375,14 @@
         c.visual = AttributeDict({})
 
         ## DB stored
-        c.visual.show_public_icon = str2bool(rc_config.get('show_public_icon'))
-        c.visual.show_private_icon = str2bool(rc_config.get('show_private_icon'))
-        c.visual.stylify_metalabels = str2bool(rc_config.get('stylify_metalabels'))
+        c.visual.show_public_icon = asbool(rc_config.get('show_public_icon'))
+        c.visual.show_private_icon = asbool(rc_config.get('show_private_icon'))
+        c.visual.stylify_metalabels = asbool(rc_config.get('stylify_metalabels'))
         c.visual.page_size = safe_int(rc_config.get('dashboard_items', 100))
         c.visual.admin_grid_items = safe_int(rc_config.get('admin_grid_items', 100))
-        c.visual.repository_fields = str2bool(rc_config.get('repository_fields'))
-        c.visual.show_version = str2bool(rc_config.get('show_version'))
-        c.visual.use_gravatar = str2bool(rc_config.get('use_gravatar'))
+        c.visual.repository_fields = asbool(rc_config.get('repository_fields'))
+        c.visual.show_version = asbool(rc_config.get('show_version'))
+        c.visual.use_gravatar = asbool(rc_config.get('use_gravatar'))
         c.visual.gravatar_url = rc_config.get('gravatar_url')
 
         c.ga_code = rc_config.get('ga_code')
@@ -404,9 +404,9 @@
         c.clone_ssh_tmpl = rc_config.get('clone_ssh_tmpl') or Repository.DEFAULT_CLONE_SSH
 
         ## INI stored
-        c.visual.allow_repo_location_change = str2bool(config.get('allow_repo_location_change', True))
-        c.visual.allow_custom_hooks_settings = str2bool(config.get('allow_custom_hooks_settings', True))
-        c.ssh_enabled = str2bool(config.get('ssh_enabled', False))
+        c.visual.allow_repo_location_change = asbool(config.get('allow_repo_location_change', True))
+        c.visual.allow_custom_hooks_settings = asbool(config.get('allow_custom_hooks_settings', True))
+        c.ssh_enabled = asbool(config.get('ssh_enabled', False))
 
         c.instance_id = config.get('instance_id')
         c.issues_url = config.get('bugtracker', url('issues_url'))
--- a/kallithea/lib/celerylib/tasks.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/celerylib/tasks.py	Wed Jun 17 21:52:16 2020 +0200
@@ -42,7 +42,7 @@
 from kallithea.lib.hooks import log_create_repository
 from kallithea.lib.rcmail.smtp_mailer import SmtpMailer
 from kallithea.lib.utils import action_logger
-from kallithea.lib.utils2 import ascii_bytes, str2bool
+from kallithea.lib.utils2 import asbool, ascii_bytes
 from kallithea.lib.vcs.utils import author_email
 from kallithea.model.db import RepoGroup, Repository, Statistics, User
 
@@ -289,9 +289,9 @@
     passwd = email_config.get('smtp_password')
     mail_server = email_config.get('smtp_server')
     mail_port = email_config.get('smtp_port')
-    tls = str2bool(email_config.get('smtp_use_tls'))
-    ssl = str2bool(email_config.get('smtp_use_ssl'))
-    debug = str2bool(email_config.get('debug'))
+    tls = asbool(email_config.get('smtp_use_tls'))
+    ssl = asbool(email_config.get('smtp_use_ssl'))
+    debug = asbool(email_config.get('debug'))
     smtp_auth = email_config.get('smtp_auth')
 
     logmsg = ("Mail details:\n"
--- a/kallithea/lib/colored_formatter.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/colored_formatter.py	Wed Jun 17 21:52:16 2020 +0200
@@ -13,6 +13,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import logging
+import sys
 
 
 BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)
@@ -65,15 +66,18 @@
     def __init__(self, *args, **kwargs):
         # can't do super(...) here because Formatter is an old school class
         logging.Formatter.__init__(self, *args, **kwargs)
+        self.plain = not getattr(sys.stderr, 'isatty', lambda: False)()
 
     def format(self, record):
         """
         Changes record's levelname to use with COLORS enum
         """
+        def_record = logging.Formatter.format(self, record)
+        if self.plain:
+            return def_record
 
         levelname = record.levelname
         start = COLOR_SEQ % (COLORS[levelname])
-        def_record = logging.Formatter.format(self, record)
         end = RESET_SEQ
 
         colored_record = ''.join([start, def_record, end])
@@ -85,14 +89,17 @@
     def __init__(self, *args, **kwargs):
         # can't do super(...) here because Formatter is an old school class
         logging.Formatter.__init__(self, *args, **kwargs)
+        self.plain = not getattr(sys.stderr, 'isatty', lambda: False)()
 
     def format(self, record):
         """
         Changes record's levelname to use with COLORS enum
         """
+        def_record = format_sql(logging.Formatter.format(self, record))
+        if self.plain:
+            return def_record
 
         start = COLOR_SEQ % (COLORS['SQL'])
-        def_record = format_sql(logging.Formatter.format(self, record))
         end = RESET_SEQ
 
         colored_record = ''.join([start, def_record, end])
--- a/kallithea/lib/db_manage.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/db_manage.py	Wed Jun 17 21:52:16 2020 +0200
@@ -37,11 +37,9 @@
 from sqlalchemy.engine import create_engine
 
 from kallithea.model.base import init_model
-from kallithea.model.db import Permission, RepoGroup, Repository, Setting, Ui, User, UserRepoGroupToPerm, UserToPerm
-#from kallithea.model import meta
+from kallithea.model.db import Repository, Setting, Ui, User
 from kallithea.model.meta import Base, Session
 from kallithea.model.permission import PermissionModel
-from kallithea.model.repo_group import RepoGroupModel
 from kallithea.model.user import UserModel
 
 
@@ -128,42 +126,6 @@
 
         log.info('Created tables for %s', self.dbname)
 
-    def fix_repo_paths(self):
-        """
-        Fixes a old kallithea version path into new one without a '*'
-        """
-
-        paths = Ui.query() \
-                .filter(Ui.ui_key == '/') \
-                .scalar()
-
-        paths.ui_value = paths.ui_value.replace('*', '')
-
-        self.sa.commit()
-
-    def fix_default_user(self):
-        """
-        Fixes a old default user with some 'nicer' default values,
-        used mostly for anonymous access
-        """
-        def_user = User.query().filter_by(is_default_user=True).one()
-
-        def_user.name = 'Anonymous'
-        def_user.lastname = 'User'
-        def_user.email = 'anonymous@kallithea-scm.org'
-
-        self.sa.commit()
-
-    def fix_settings(self):
-        """
-        Fixes kallithea settings adds ga_code key for google analytics
-        """
-
-        hgsettings3 = Setting('ga_code', '')
-
-        self.sa.add(hgsettings3)
-        self.sa.commit()
-
     def admin_prompt(self, second=False):
         if not self.tests:
             import getpass
@@ -244,45 +206,6 @@
             setting = Setting(k, v, t)
             self.sa.add(setting)
 
-    def fixup_groups(self):
-        def_usr = User.get_default_user()
-        for g in RepoGroup.query().all():
-            g.group_name = g.get_new_name(g.name)
-            # get default perm
-            default = UserRepoGroupToPerm.query() \
-                .filter(UserRepoGroupToPerm.group == g) \
-                .filter(UserRepoGroupToPerm.user == def_usr) \
-                .scalar()
-
-            if default is None:
-                log.debug('missing default permission for group %s adding', g)
-                RepoGroupModel()._create_default_perms(g)
-
-    def reset_permissions(self, username):
-        """
-        Resets permissions to default state, useful when old systems had
-        bad permissions, we must clean them up
-
-        :param username:
-        """
-        default_user = User.get_by_username(username)
-        if not default_user:
-            return
-
-        u2p = UserToPerm.query() \
-            .filter(UserToPerm.user == default_user).all()
-        fixed = False
-        if len(u2p) != len(Permission.DEFAULT_USER_PERMISSIONS):
-            for p in u2p:
-                Session().delete(p)
-            fixed = True
-            self.populate_default_permissions()
-        return fixed
-
-    def update_repo_info(self):
-        for repo in Repository.query():
-            repo.update_changeset_cache()
-
     def prompt_repo_root_path(self, test_repo_path='', retries=3):
         _path = self.cli_args.get('repos_location')
         if retries == 3:
--- a/kallithea/lib/diffs.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/diffs.py	Wed Jun 17 21:52:16 2020 +0200
@@ -89,10 +89,10 @@
         'table_class': table_class
     })
 
-    for diff in parsed_lines:
-        for line in diff['chunks']:
+    for file_info in parsed_lines:
+        for chunk in file_info['chunks']:
             _html_empty = False
-            for change in line:
+            for change in chunk:
                 _html.append('''<tr class="%(lc)s %(action)s">\n''' % {
                     'lc': line_class,
                     'action': change['action']
@@ -100,19 +100,16 @@
                 anchor_old_id = ''
                 anchor_new_id = ''
                 anchor_old = "%(filename)s_o%(oldline_no)s" % {
-                    'filename': _safe_id(diff['filename']),
+                    'filename': _safe_id(file_info['filename']),
                     'oldline_no': change['old_lineno']
                 }
-                anchor_new = "%(filename)s_n%(oldline_no)s" % {
-                    'filename': _safe_id(diff['filename']),
-                    'oldline_no': change['new_lineno']
+                anchor_new = "%(filename)s_n%(newline_no)s" % {
+                    'filename': _safe_id(file_info['filename']),
+                    'newline_no': change['new_lineno']
                 }
-                cond_old = (change['old_lineno'] != '...' and
-                            change['old_lineno'])
-                cond_new = (change['new_lineno'] != '...' and
-                            change['new_lineno'])
-                no_lineno = (change['old_lineno'] == '...' and
-                             change['new_lineno'] == '...')
+                cond_old = change['old_lineno']
+                cond_new = change['new_lineno']
+                no_lineno = not change['old_lineno'] and not change['new_lineno']
                 if cond_old:
                     anchor_old_id = 'id="%s"' % anchor_old
                 if cond_new:
@@ -453,7 +450,7 @@
         return self.adds, self.removes
 
 
-_escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)')
+_escape_re = re.compile(r'(&)|(<)|(>)|(\t)|(\r)|(?<=.)( \n| $)|(\t\n|\t$)')
 
 
 def _escaper(string):
@@ -470,11 +467,13 @@
         if groups[2]:
             return '&gt;'
         if groups[3]:
-            return '<u>\t</u>'
+            return '<u>\t</u>'  # Note: trailing tabs will get a longer match later
         if groups[4]:
             return '<u class="cr"></u>'
         if groups[5]:
             return ' <i></i>'
+        if groups[6]:
+            return '<u>\t</u><i></i>'
         assert False
 
     return _escape_re.sub(substitute, safe_str(string))
@@ -585,8 +584,8 @@
                 # skip context only if it's first line
                 if int(gr[0]) > 1:
                     lines.append({
-                        'old_lineno': '...',
-                        'new_lineno': '...',
+                        'old_lineno': '',
+                        'new_lineno': '',
                         'action':     'context',
                         'line':       line,
                     })
@@ -630,8 +629,8 @@
                     # we need to append to lines, since this is not
                     # counted in the line specs of diff
                     lines.append({
-                        'old_lineno':   '...',
-                        'new_lineno':   '...',
+                        'old_lineno':   '',
+                        'new_lineno':   '',
                         'action':       'context',
                         'line':         line,
                     })
--- a/kallithea/lib/helpers.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/helpers.py	Wed Jun 17 21:52:16 2020 +0200
@@ -48,7 +48,7 @@
 from kallithea.lib.pygmentsutils import get_custom_lexer
 from kallithea.lib.utils2 import MENTIONS_REGEX, AttributeDict
 from kallithea.lib.utils2 import age as _age
-from kallithea.lib.utils2 import credentials_filter, safe_bytes, safe_int, safe_str, str2bool, time_to_datetime
+from kallithea.lib.utils2 import asbool, credentials_filter, safe_bytes, safe_int, safe_str, time_to_datetime
 from kallithea.lib.vcs.backends.base import BaseChangeset, EmptyChangeset
 from kallithea.lib.vcs.exceptions import ChangesetDoesNotExistError
 #==============================================================================
@@ -526,7 +526,7 @@
     """
     from kallithea import CONFIG
     def_len = safe_int(CONFIG.get('show_sha_length', 12))
-    show_rev = str2bool(CONFIG.get('show_revision_number', False))
+    show_rev = asbool(CONFIG.get('show_revision_number', False))
 
     raw_id = cs.raw_id[:def_len]
     if show_rev:
@@ -932,7 +932,7 @@
     else:
         # if src is empty then there was no gravatar, so we use a font icon
         html = ("""<i class="icon-user {cls}" style="font-size: {size}px;"></i>"""
-            .format(cls=cls, size=size, src=src))
+            .format(cls=cls, size=size))
 
     return literal(html)
 
--- a/kallithea/lib/hooks.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/hooks.py	Wed Jun 17 21:52:16 2020 +0200
@@ -307,14 +307,14 @@
     connect to the database.
     """
     import paste.deploy
-    import kallithea.config.middleware
+    import kallithea.config.application
 
     extras = get_hook_environment()
 
     path_to_ini_file = extras['config']
     kallithea.CONFIG = paste.deploy.appconfig('config:' + path_to_ini_file)
     #logging.config.fileConfig(ini_file_path) # Note: we are in a different process - don't use configured logging
-    kallithea.config.middleware.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
+    kallithea.config.application.make_app(kallithea.CONFIG.global_conf, **kallithea.CONFIG.local_conf)
 
     # fix if it's not a bare repo
     if repo_path.endswith(os.sep + '.git'):
--- a/kallithea/lib/inifile.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/inifile.py	Wed Jun 17 21:52:16 2020 +0200
@@ -119,9 +119,6 @@
     #variable7 = 7.1
     #variable8 = 8.0
     <BLANKLINE>
-    variable8 = None
-    variable9 = None
-    <BLANKLINE>
     [fourth-section]
     fourth = "four"
     fourth_extra = 4
@@ -180,7 +177,7 @@
                 new_value = section_settings[key]
                 if new_value == line_value:
                     line = line.lstrip('#')
-                else:
+                elif new_value is not None:
                     line += '\n%s = %s' % (key, new_value)
                 section_settings.pop(key)
                 return line
@@ -189,8 +186,12 @@
 
             # 3rd pass:
             # settings that haven't been consumed yet at is appended to section
-            if section_settings:
-                lines += '\n' + ''.join('%s = %s\n' % (key, value) for key, value in sorted(section_settings.items()))
+            append_lines = ''.join(
+                '%s = %s\n' % (key, value)
+                for key, value in sorted(section_settings.items())
+                if value is not None)
+            if append_lines:
+                lines += '\n' + append_lines
 
         return sectionname + '\n' + re.sub('[ \t]+\n', '\n', lines)
 
--- a/kallithea/lib/middleware/https_fixup.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/middleware/https_fixup.py	Wed Jun 17 21:52:16 2020 +0200
@@ -26,7 +26,7 @@
 """
 
 
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 
 
 class HttpsFixup(object):
@@ -37,11 +37,11 @@
 
     def __call__(self, environ, start_response):
         self.__fixup(environ)
-        debug = str2bool(self.config.get('debug'))
+        debug = asbool(self.config.get('debug'))
         is_ssl = environ['wsgi.url_scheme'] == 'https'
 
         def custom_start_response(status, headers, exc_info=None):
-            if is_ssl and str2bool(self.config.get('use_htsts')) and not debug:
+            if is_ssl and asbool(self.config.get('use_htsts')) and not debug:
                 headers.append(('Strict-Transport-Security',
                                 'max-age=8640000; includeSubDomains'))
             return start_response(status, headers, exc_info)
@@ -66,7 +66,7 @@
         org_proto = proto
 
         # if we have force, just override
-        if str2bool(self.config.get('force_https')):
+        if asbool(self.config.get('force_https')):
             proto = 'https'
 
         environ['wsgi.url_scheme'] = proto
--- a/kallithea/lib/paster_commands/template.ini.mako	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/paster_commands/template.ini.mako	Wed Jun 17 21:52:16 2020 +0200
@@ -69,7 +69,7 @@
 port = ${port}
 
 %if http_server == 'gearbox':
-<%text>##</%text> Gearbox default web server ##
+<%text>##</%text> Gearbox serve uses the built-in development web server ##
 use = egg:gearbox#wsgiref
 <%text>##</%text> nr of worker threads to spawn
 threadpool_workers = 1
@@ -79,22 +79,22 @@
 use_threadpool = true
 
 %elif http_server == 'gevent':
-<%text>##</%text> Gearbox gevent web server ##
+<%text>##</%text> Gearbox serve uses the gevent web server ##
 use = egg:gearbox#gevent
 
 %elif http_server == 'waitress':
-<%text>##</%text> WAITRESS ##
+<%text>##</%text> Gearbox serve uses the Waitress web server ##
 use = egg:waitress#main
-<%text>##</%text> number of worker threads
+<%text>##</%text> avoid multi threading
 threads = 1
-<%text>##</%text> MAX BODY SIZE 100GB
+<%text>##</%text> allow push of repos bigger than the default of 1 GB
 max_request_body_size = 107374182400
 <%text>##</%text> use poll instead of select, fixes fd limits, may not work on old
 <%text>##</%text> windows systems.
 #asyncore_use_poll = True
 
 %elif http_server == 'gunicorn':
-<%text>##</%text> GUNICORN ##
+<%text>##</%text> Gearbox serve uses the Gunicorn web server ##
 use = egg:gunicorn#main
 <%text>##</%text> number of process workers. You must set `instance_id = *` when this option
 <%text>##</%text> is set to more than one worker
@@ -453,21 +453,22 @@
 <%text>##</%text>#######################
 
 %if database_engine == 'sqlite':
-<%text>##</%text> SQLITE [default]
 sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
-
-%elif database_engine == 'postgres':
-<%text>##</%text> POSTGRESQL
+%else:
+#sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
+%endif
+%if database_engine == 'postgres':
 sqlalchemy.url = postgresql://user:pass@localhost/kallithea
-
-%elif database_engine == 'mysql':
-<%text>##</%text> MySQL
+%else:
+#sqlalchemy.url = postgresql://user:pass@localhost/kallithea
+%endif
+%if database_engine == 'mysql':
 sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
+%else:
+#sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
+%endif
 <%text>##</%text> Note: the mysql:// prefix should also be used for MariaDB
 
-%endif
-<%text>##</%text> see sqlalchemy docs for other backends
-
 sqlalchemy.pool_recycle = 3600
 
 <%text>##</%text>##############################
--- a/kallithea/lib/utils2.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/utils2.py	Wed Jun 17 21:52:16 2020 +0200
@@ -38,6 +38,7 @@
 import urlobject
 from tg.i18n import ugettext as _
 from tg.i18n import ungettext
+from tg.support.converters import asbool, aslist
 from webhelpers2.text import collapse, remove_formatting, strip_tags
 
 from kallithea.lib.vcs.utils import ascii_bytes, ascii_str, safe_bytes, safe_str  # re-export
@@ -51,6 +52,8 @@
 
 
 # mute pyflakes "imported but unused"
+assert asbool
+assert aslist
 assert ascii_bytes
 assert ascii_str
 assert safe_bytes
@@ -58,44 +61,6 @@
 assert LazyProperty
 
 
-def str2bool(_str):
-    """
-    returns True/False value from given string, it tries to translate the
-    string into boolean
-
-    :param _str: string value to translate into boolean
-    :rtype: boolean
-    :returns: boolean from given string
-    """
-    if _str is None:
-        return False
-    if _str in (True, False):
-        return _str
-    _str = str(_str).strip().lower()
-    return _str in ('t', 'true', 'y', 'yes', 'on', '1')
-
-
-def aslist(obj, sep=None, strip=True):
-    """
-    Returns given string separated by sep as list
-
-    :param obj:
-    :param sep:
-    :param strip:
-    """
-    if isinstance(obj, (str)):
-        lst = obj.split(sep)
-        if strip:
-            lst = [v.strip() for v in lst]
-        return lst
-    elif isinstance(obj, (list, tuple)):
-        return obj
-    elif obj is None:
-        return []
-    else:
-        return [obj]
-
-
 def convert_line_endings(line, mode):
     """
     Converts a given line  "line end" according to given mode
--- a/kallithea/lib/vcs/backends/hg/repository.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/repository.py	Wed Jun 17 21:52:16 2020 +0200
@@ -272,7 +272,7 @@
             self.get_changeset(rev1)
         self.get_changeset(rev2)
         if path:
-            file_filter = mercurial.match.exact(path)
+            file_filter = mercurial.match.exact([safe_bytes(path)])
         else:
             file_filter = None
 
--- a/kallithea/model/comment.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/comment.py	Wed Jun 17 21:52:16 2020 +0200
@@ -105,6 +105,7 @@
                 'message': cs.message,
                 'message_short': h.shorter(cs.message, 50, firstline=True),
                 'cs_author': cs_author,
+                'cs_author_username': cs_author.username,
                 'repo_name': repo.repo_name,
                 'short_id': h.short_id(revision),
                 'branch': cs.branch,
--- a/kallithea/model/db.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/db.py	Wed Jun 17 21:52:16 2020 +0200
@@ -46,7 +46,7 @@
 import kallithea
 from kallithea.lib import ext_json
 from kallithea.lib.exceptions import DefaultUserException
-from kallithea.lib.utils2 import (Optional, ascii_bytes, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_bytes, safe_int, safe_str, str2bool,
+from kallithea.lib.utils2 import (Optional, asbool, ascii_bytes, aslist, get_changeset_safe, get_clone_url, remove_prefix, safe_bytes, safe_int, safe_str,
                                   urlreadable)
 from kallithea.lib.vcs import get_backend
 from kallithea.lib.vcs.backends.base import EmptyChangeset
@@ -61,10 +61,6 @@
 # BASE CLASSES
 #==============================================================================
 
-def _hash_key(k):
-    return hashlib.md5(safe_bytes(k)).hexdigest()
-
-
 class BaseDbModel(object):
     """
     Base Model for all classes
@@ -185,7 +181,7 @@
         'str': safe_bytes,
         'int': safe_int,
         'unicode': safe_str,
-        'bool': str2bool,
+        'bool': asbool,
         'list': functools.partial(aslist, sep=',')
     }
     DEFAULT_UPDATE_URL = ''
@@ -1164,7 +1160,7 @@
         if with_pullrequests:
             data['pull_requests'] = repo.pull_requests_other
         rc_config = Setting.get_app_settings()
-        repository_fields = str2bool(rc_config.get('repository_fields'))
+        repository_fields = asbool(rc_config.get('repository_fields'))
         if repository_fields:
             for f in self.extra_fields:
                 data[f.field_key_prefixed] = f.field_value
@@ -1556,18 +1552,12 @@
         ('usergroup.write', _('Default user has write access to new user groups')),
         ('usergroup.admin', _('Default user has admin access to new user groups')),
 
-        ('hg.repogroup.create.false', _('Only admins can create repository groups')),
-        ('hg.repogroup.create.true', _('Non-admins can create repository groups')),
-
         ('hg.usergroup.create.false', _('Only admins can create user groups')),
         ('hg.usergroup.create.true', _('Non-admins can create user groups')),
 
         ('hg.create.none', _('Only admins can create top level repositories')),
         ('hg.create.repository', _('Non-admins can create top level repositories')),
 
-        ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
-        ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
-
         ('hg.fork.none', _('Only admins can fork repositories')),
         ('hg.fork.repository', _('Non-admins can fork repositories')),
 
@@ -1585,7 +1575,6 @@
         'group.read',
         'usergroup.read',
         'hg.create.repository',
-        'hg.create.write_on_repogroup.true',
         'hg.fork.repository',
         'hg.register.manual_activate',
         'hg.extern_activate.auto',
@@ -1610,9 +1599,6 @@
         'usergroup.write': 3,
         'usergroup.admin': 4,
 
-        'hg.repogroup.create.false': 0,
-        'hg.repogroup.create.true': 1,
-
         'hg.usergroup.create.false': 0,
         'hg.usergroup.create.true': 1,
 
@@ -1622,9 +1608,6 @@
         'hg.create.none': 0,
         'hg.create.repository': 1,
 
-        'hg.create.write_on_repogroup.false': 0,
-        'hg.create.write_on_repogroup.true': 1,
-
         'hg.register.none': 0,
         'hg.register.manual_activate': 1,
         'hg.register.auto_activate': 2,
--- a/kallithea/model/forms.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/forms.py	Wed Jun 17 21:52:16 2020 +0200
@@ -396,7 +396,7 @@
 
 def DefaultPermissionsForm(repo_perms_choices, group_perms_choices,
                            user_group_perms_choices, create_choices,
-                           create_on_write_choices, repo_group_create_choices,
+                           repo_group_create_choices,
                            user_group_create_choices, fork_choices,
                            register_choices, extern_activate_choices):
     class _DefaultPermissionsForm(formencode.Schema):
@@ -411,9 +411,7 @@
         default_user_group_perm = v.OneOf(user_group_perms_choices)
 
         default_repo_create = v.OneOf(create_choices)
-        create_on_write = v.OneOf(create_on_write_choices)
         default_user_group_create = v.OneOf(user_group_create_choices)
-        #default_repo_group_create = v.OneOf(repo_group_create_choices) #not impl. yet
         default_fork = v.OneOf(fork_choices)
 
         default_register = v.OneOf(register_choices)
--- a/kallithea/model/notification.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/notification.py	Wed Jun 17 21:52:16 2020 +0200
@@ -126,12 +126,18 @@
         email_html_body = EmailNotificationModel() \
                             .get_email_tmpl(type_, 'html', **html_kwargs)
 
-        # don't send email to person who created this comment
-        rec_objs = set(recipients_objs).difference(set([created_by_obj]))
+        # don't send email to the person who caused the notification, except for
+        # notifications about new pull requests where the author is explicitly
+        # added.
+        rec_mails = set(obj.email for obj in recipients_objs)
+        if type_ == NotificationModel.TYPE_PULL_REQUEST:
+            rec_mails.add(created_by_obj.email)
+        else:
+            rec_mails.discard(created_by_obj.email)
 
-        # send email with notification to all other participants
-        for rec in rec_objs:
-            tasks.send_email([rec.email], email_subject, email_txt_body,
+        # send email with notification to participants
+        for rec_mail in sorted(rec_mails):
+            tasks.send_email([rec_mail], email_subject, email_txt_body,
                      email_html_body, headers,
                      from_name=created_by_obj.full_name_or_username)
 
@@ -159,7 +165,7 @@
             self.TYPE_PULL_REQUEST_COMMENT: 'pull_request_comment',
         }
         self._subj_map = {
-            self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s'),
+            self.TYPE_CHANGESET_COMMENT: _('[Comment] %(repo_name)s changeset %(short_id)s "%(message_short)s" on %(branch)s by %(cs_author_username)s'),
             self.TYPE_MESSAGE: 'Test Message',
             # self.TYPE_PASSWORD_RESET
             self.TYPE_REGISTRATION: _('New user %(new_username)s registered'),
--- a/kallithea/model/permission.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/permission.py	Wed Jun 17 21:52:16 2020 +0200
@@ -31,7 +31,7 @@
 
 from sqlalchemy.exc import DatabaseError
 
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.model.db import Permission, Session, User, UserRepoGroupToPerm, UserRepoToPerm, UserToPerm, UserUserGroupToPerm
 
 
@@ -97,7 +97,7 @@
         try:
             # stage 1 set anonymous access
             if perm_user.is_default_user:
-                perm_user.active = str2bool(form_result['anonymous'])
+                perm_user.active = asbool(form_result['anonymous'])
 
             # stage 2 reset defaults and set them from form data
             def _make_new(usr, perm_name):
@@ -119,8 +119,6 @@
                                  'default_group_perm',
                                  'default_user_group_perm',
                                  'default_repo_create',
-                                 'create_on_write', # special case for create repos on write access to group
-                                 #'default_repo_group_create', # not implemented yet
                                  'default_user_group_create',
                                  'default_fork',
                                  'default_register',
--- a/kallithea/model/scm.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/scm.py	Wed Jun 17 21:52:16 2020 +0200
@@ -748,7 +748,7 @@
                 log.debug('skipping writing hook file')
 
 
-def AvailableRepoGroupChoices(top_perms, repo_group_perm_level, extras=()):
+def AvailableRepoGroupChoices(repo_group_perm_level, extras=()):
     """Return group_id,string tuples with choices for all the repo groups where
     the user has the necessary permissions.
 
@@ -759,7 +759,7 @@
         groups.append(None)
     else:
         groups = list(RepoGroupList(groups, perm_level=repo_group_perm_level))
-        if top_perms and HasPermissionAny(*top_perms)('available repo groups'):
+        if HasPermissionAny('hg.create.repository')('available repo groups'):
             groups.append(None)
         for extra in extras:
             if not any(rg == extra for rg in groups):
--- a/kallithea/model/ssh_key.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/ssh_key.py	Wed Jun 17 21:52:16 2020 +0200
@@ -29,7 +29,7 @@
 from tg.i18n import ugettext as _
 
 from kallithea.lib import ssh
-from kallithea.lib.utils2 import str2bool
+from kallithea.lib.utils2 import asbool
 from kallithea.lib.vcs.exceptions import RepositoryError
 from kallithea.model.db import User, UserSshKeys
 from kallithea.model.meta import Session
@@ -95,7 +95,7 @@
         return user_ssh_keys
 
     def write_authorized_keys(self):
-        if not str2bool(config.get('ssh_enabled', False)):
+        if not asbool(config.get('ssh_enabled', False)):
             log.error("Will not write SSH authorized_keys file - ssh_enabled is not configured")
             return
         authorized_keys = config.get('ssh_authorized_keys')
--- a/kallithea/model/validators.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/model/validators.py	Wed Jun 17 21:52:16 2020 +0200
@@ -32,7 +32,7 @@
 from kallithea.lib.compat import OrderedSet
 from kallithea.lib.exceptions import InvalidCloneUriException, LdapImportError
 from kallithea.lib.utils import is_valid_repo_uri
-from kallithea.lib.utils2 import aslist, repo_name_slug, str2bool
+from kallithea.lib.utils2 import asbool, aslist, repo_name_slug
 from kallithea.model import db
 from kallithea.model.db import RepoGroup, Repository, User, UserGroup
 
@@ -456,12 +456,11 @@
             gr_name = gr.group_name if gr is not None else None # None means ROOT location
 
             # create repositories with write permission on group is set to true
-            create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
             group_admin = HasRepoGroupPermissionLevel('admin')(gr_name,
                                             'can write into group validator')
             group_write = HasRepoGroupPermissionLevel('write')(gr_name,
                                             'can write into group validator')
-            forbidden = not (group_admin or (group_write and create_on_write))
+            forbidden = not (group_admin or group_write)
             can_create_repos = HasPermissionAny('hg.admin', 'hg.create.repository')
             gid = (old_data['repo_group'].get('group_id')
                    if (old_data and 'repo_group' in old_data) else None)
@@ -568,7 +567,7 @@
                          'g': 'users_group'
                     }[k[0]]
                     if member_name == User.DEFAULT_USER_NAME:
-                        if str2bool(value.get('repo_private')):
+                        if asbool(value.get('repo_private')):
                             # set none for default when updating to
                             # private repo protects against form manipulation
                             v = EMPTY_PERM
--- a/kallithea/templates/admin/permissions/permissions_globals.html	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/templates/admin/permissions/permissions_globals.html	Wed Jun 17 21:52:16 2020 +0200
@@ -58,13 +58,6 @@
                 </div>
             </div>
             <div class="form-group">
-                <label class="control-label" for="create_on_write">${_('Repository creation with group write access')}:</label>
-                <div>
-                    ${h.select('create_on_write','',c.repo_create_on_write_choices,class_='form-control')}
-                    <span class="help-block">${_('With this, write permission to a repository group allows creating repositories inside that group. Without this, group write permissions mean nothing.')}</span>
-                </div>
-            </div>
-            <div class="form-group">
                 <label class="control-label" for="default_user_group_create">${_('User group creation')}:</label>
                 <div>
                     ${h.select('default_user_group_create','',c.user_group_create_choices,class_='form-control')}
--- a/kallithea/templates/index_base.html	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/templates/index_base.html	Wed Jun 17 21:52:16 2020 +0200
@@ -16,11 +16,10 @@
                 <%
                     gr_name = c.group.group_name if c.group else None
                     # create repositories with write permission on group is set to true
-                    create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
                     group_admin = h.HasRepoGroupPermissionLevel('admin')(gr_name, 'can write into group index page')
                     group_write = h.HasRepoGroupPermissionLevel('write')(gr_name, 'can write into group index page')
                 %>
-                %if h.HasPermissionAny('hg.admin','hg.create.repository')() or (group_admin or (group_write and create_on_write)):
+                %if h.HasPermissionAny('hg.admin','hg.create.repository')() or group_admin or group_write:
                   %if c.group:
                         <a href="${h.url('new_repo',parent_group=c.group.group_id)}" class="btn btn-default btn-xs"><i class="icon-plus"></i>${_('Add Repository')}</a>
                         %if h.HasPermissionAny('hg.admin')() or h.HasRepoGroupPermissionLevel('admin')(c.group.group_name):
--- a/kallithea/tests/models/test_diff_parsers.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/models/test_diff_parsers.py	Wed Jun 17 21:52:16 2020 +0200
@@ -295,7 +295,7 @@
             l.append('%(action)-7s %(new_lineno)3s %(old_lineno)3s %(line)r\n' % d)
         s = ''.join(l)
         assert s == r'''
-context ... ... '@@ -51,6 +51,13 @@\n'
+context         '@@ -51,6 +51,13 @@\n'
 unmod    51  51 '<u>\t</u>begin();\n'
 unmod    52  52 '<u>\t</u>\n'
 add      53     '<u>\t</u>int foo;<u class="cr"></u>\n'
--- a/kallithea/tests/models/test_dump_html_mails.ref.html	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/models/test_dump_html_mails.ref.html	Wed Jun 17 21:52:16 2020 +0200
@@ -7,7 +7,7 @@
 <pre>
 From: u1 u1 <name@example.com>
 To: u2@example.com
-Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
+Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 </pre>
 <hr/>
 <pre>http://comment.org
@@ -166,7 +166,7 @@
 <pre>
 From: u1 u1 <name@example.com>
 To: u2@example.com
-Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
+Subject: [Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 </pre>
 <hr/>
 <pre>http://comment.org
@@ -325,7 +325,7 @@
 <pre>
 From: u1 u1 <name@example.com>
 To: u2@example.com
-Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
+Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 </pre>
 <hr/>
 <pre>http://comment.org
@@ -502,7 +502,7 @@
 <pre>
 From: u1 u1 <name@example.com>
 To: u2@example.com
-Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch
+Subject: [Approved: Comment] repo/name changeset cafe1234 "This changeset did something cl..." on brunch by u2
 </pre>
 <hr/>
 <pre>http://comment.org
@@ -882,6 +882,197 @@
 <h1>pull_request, is_mention=False</h1>
 <pre>
 From: u1 u1 <name@example.com>
+To: u1@example.com
+Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
+</pre>
+<hr/>
+<pre>http://pr.org/7
+
+Added as Reviewer of Pull Request #7 "The Title" by Requesting User (root)
+
+
+Pull request #7 "The Title" by u2 u3 (u2)
+from https://dev.org/repo branch devbranch
+to http://mainline.com/repo branch trunk
+
+
+Description:
+
+This PR is 'awesome' because it does <stuff>
+ - please approve indented!
+
+
+Changesets:
+
+Introduce one and two
+Make one plus two equal tree
+
+
+View Pull Request: http://pr.org/7
+</pre>
+<hr/>
+<!--!doctype html-->
+<!--html lang="en"-->
+<!--head-->
+    <!--title--><!--/title-->
+    <!--meta name="viewport" content="width=device-width"-->
+    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
+<!--/head-->
+<!--body-->
+<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
+    <tr>
+        <td width="30px" style="width:30px"></td>
+        <td>
+            <table width="100%" cellpadding="0" cellspacing="0" border="0"
+                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
+                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
+                <tr>
+                    <td colspan="3">
+<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
+       style="border-bottom:1px solid #ddd">
+    <tr>
+        <td height="20px" style="height:20px" colspan="3"></td>
+    </tr>
+    <tr>
+        <td width="30px" style="width:30px"></td>
+        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
+            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://pr.org/7"
+               target="_blank">Added as Reviewer of Pull Request #7 &#34;The Title&#34; by Requesting User (root)</a>
+        </td>
+        <td width="30px" style="width:30px"></td>
+    </tr>
+    <tr>
+        <td height="20px" style="height:20px" colspan="3"></td>
+    </tr>
+</table>
+                    </td>
+                </tr>
+                <tr>
+                    <td height="30px" style="height:30px" colspan="3"></td>
+                </tr>
+                <tr>
+                    <td></td>
+                    <td>
+<table cellpadding="0" cellspacing="0" border="0" width="100%">
+    <tr>
+        <td>
+            <div>
+                Pull request
+                <a style="color:#395fa0;text-decoration:none"
+                   href="http://pr.org/7">#7 "The Title"</a>
+                by
+                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
+            </div>
+            <div>
+                from
+                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
+                   href="https://dev.org/repo">https://dev.org/repo</a>
+                branch
+                <span style="border:#ddd 1px solid;background:#f9f9f9">devbranch</span>
+                <br/>
+                to
+                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
+                   href="http://mainline.com/repo">http://mainline.com/repo</a>
+                branch
+                <span style="border:#ddd 1px solid;background:#f9f9f9">trunk</span>
+            </div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+    <tr>
+        <td>
+            <div>
+                Description:
+            </div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+    <tr>
+        <td>
+            <table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
+                <tr>
+                    <td height="10px" style="height:10px" colspan="3"></td>
+                </tr>
+                <tr>
+                    <td width="20px" style="width:20px"></td>
+                    <td>
+                        <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This PR is &#39;awesome&#39; because it does &lt;stuff&gt;<br/> - please approve indented!</div></div>
+                    </td>
+                    <td width="20px" style="width:20px"></td>
+                </tr>
+                <tr>
+                    <td height="10px" style="height:10px" colspan="3"></td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr><td height="15px" style="height:15px"></td></tr>
+    <tr>
+        <td>
+            <div>Changesets:</div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+
+    <tr>
+        <td style="font-family:Helvetica,Arial,sans-serif">
+            <ul style="color:#395fa0;padding-left:15px;margin:0">
+                    <li style="mso-special-format:bullet">
+                        <a style="color:#395fa0;text-decoration:none"
+                           href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">
+                            Introduce one and two
+                        </a>
+                    </li>
+                    <li style="mso-special-format:bullet">
+                        <a style="color:#395fa0;text-decoration:none"
+                           href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">
+                            Make one plus two equal tree
+                        </a>
+                    </li>
+            </ul>
+        </td>
+    </tr>
+    <tr>
+        <td>
+<center>
+    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
+        <tr>
+            <td height="25px" style="height:25px"></td>
+        </tr>
+        <tr>
+            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
+                <a href="http://pr.org/7" style="text-decoration:none;display:block" target="_blank">
+                    <center>
+                        <font size="3">
+                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Pull Request</span>
+                        </font>
+                    </center>
+                </a>
+            </td>
+        </tr>
+    </table>
+</center>
+        </td>
+    </tr>
+</table>
+                    </td>
+                    <td></td>
+                </tr>
+                <tr>
+                    <td height="30px" style="height:30px" colspan="3"></td>
+                </tr>
+            </table>
+        </td>
+        <td width="30px" style="width:30px"></td>
+    </tr>
+</table>
+<!--/body-->
+<!--/html-->
+<hr/>
+<hr/>
+<h1>pull_request, is_mention=False</h1>
+<pre>
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
@@ -1073,6 +1264,197 @@
 <h1>pull_request, is_mention=True</h1>
 <pre>
 From: u1 u1 <name@example.com>
+To: u1@example.com
+Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
+</pre>
+<hr/>
+<pre>http://pr.org/7
+
+Mention on Pull Request #7 "The Title" by Requesting User (root)
+
+
+Pull request #7 "The Title" by u2 u3 (u2)
+from https://dev.org/repo branch devbranch
+to http://mainline.com/repo branch trunk
+
+
+Description:
+
+This PR is 'awesome' because it does <stuff>
+ - please approve indented!
+
+
+Changesets:
+
+Introduce one and two
+Make one plus two equal tree
+
+
+View Pull Request: http://pr.org/7
+</pre>
+<hr/>
+<!--!doctype html-->
+<!--html lang="en"-->
+<!--head-->
+    <!--title--><!--/title-->
+    <!--meta name="viewport" content="width=device-width"-->
+    <!--meta http-equiv="Content-Type" content="text/html; charset=UTF-8"-->
+<!--/head-->
+<!--body-->
+<table align="center" cellpadding="0" cellspacing="0" border="0" style="min-width:348px;max-width:800px;font-family:Helvetica,Arial,sans-serif;font-weight:200;font-size:14px;line-height:17px;color:#202020">
+    <tr>
+        <td width="30px" style="width:30px"></td>
+        <td>
+            <table width="100%" cellpadding="0" cellspacing="0" border="0"
+                   style="table-layout:fixed;font-family:Helvetica,Arial,sans-serif;border:1px solid #ddd">
+                <tr><td width="30px" style="width:30px"></td><td></td><td width="30px" style="width:30px"></td></tr>
+                <tr>
+                    <td colspan="3">
+<table bgcolor="#f9f9f9" width="100%" cellpadding="0" cellspacing="0"
+       style="border-bottom:1px solid #ddd">
+    <tr>
+        <td height="20px" style="height:20px" colspan="3"></td>
+    </tr>
+    <tr>
+        <td width="30px" style="width:30px"></td>
+        <td style="font-family:Helvetica,Arial,sans-serif;font-size:19px;line-height:24px">
+            <a style="text-decoration:none;font-weight:600;color:#395fa0" href="http://pr.org/7"
+               target="_blank">Mention on Pull Request #7 &#34;The Title&#34; by Requesting User (root)</a>
+        </td>
+        <td width="30px" style="width:30px"></td>
+    </tr>
+    <tr>
+        <td height="20px" style="height:20px" colspan="3"></td>
+    </tr>
+</table>
+                    </td>
+                </tr>
+                <tr>
+                    <td height="30px" style="height:30px" colspan="3"></td>
+                </tr>
+                <tr>
+                    <td></td>
+                    <td>
+<table cellpadding="0" cellspacing="0" border="0" width="100%">
+    <tr>
+        <td>
+            <div>
+                Pull request
+                <a style="color:#395fa0;text-decoration:none"
+                   href="http://pr.org/7">#7 "The Title"</a>
+                by
+                <span style="border:#ddd 1px solid;background:#f9f9f9">u2 u3 (u2)</span>.
+            </div>
+            <div>
+                from
+                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
+                   href="https://dev.org/repo">https://dev.org/repo</a>
+                branch
+                <span style="border:#ddd 1px solid;background:#f9f9f9">devbranch</span>
+                <br/>
+                to
+                <a style="color:#202020;text-decoration:none;border:#ddd 1px solid;background:#f9f9f9"
+                   href="http://mainline.com/repo">http://mainline.com/repo</a>
+                branch
+                <span style="border:#ddd 1px solid;background:#f9f9f9">trunk</span>
+            </div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+    <tr>
+        <td>
+            <div>
+                Description:
+            </div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+    <tr>
+        <td>
+            <table cellpadding="0" cellspacing="0" width="100%" border="0" bgcolor="#f9f9f9" style="border:1px solid #ddd;border-radius:4px">
+                <tr>
+                    <td height="10px" style="height:10px" colspan="3"></td>
+                </tr>
+                <tr>
+                    <td width="20px" style="width:20px"></td>
+                    <td>
+                        <div style="font-family:Lucida Console,Consolas,Monaco,Inconsolata,Liberation Mono,monospace;white-space:pre-wrap"><div class="formatted-fixed">This PR is &#39;awesome&#39; because it does &lt;stuff&gt;<br/> - please approve indented!</div></div>
+                    </td>
+                    <td width="20px" style="width:20px"></td>
+                </tr>
+                <tr>
+                    <td height="10px" style="height:10px" colspan="3"></td>
+                </tr>
+            </table>
+        </td>
+    </tr>
+    <tr><td height="15px" style="height:15px"></td></tr>
+    <tr>
+        <td>
+            <div>Changesets:</div>
+        </td>
+    </tr>
+    <tr><td height="10px" style="height:10px"></td></tr>
+
+    <tr>
+        <td style="font-family:Helvetica,Arial,sans-serif">
+            <ul style="color:#395fa0;padding-left:15px;margin:0">
+                    <li style="mso-special-format:bullet">
+                        <a style="color:#395fa0;text-decoration:none"
+                           href="http://changeset_home/?repo_name=repo_org&amp;revision=123abc123abc123abc123abc123abc123abc123abc">
+                            Introduce one and two
+                        </a>
+                    </li>
+                    <li style="mso-special-format:bullet">
+                        <a style="color:#395fa0;text-decoration:none"
+                           href="http://changeset_home/?repo_name=repo_org&amp;revision=567fed567fed567fed567fed567fed567fed567fed">
+                            Make one plus two equal tree
+                        </a>
+                    </li>
+            </ul>
+        </td>
+    </tr>
+    <tr>
+        <td>
+<center>
+    <table cellspacing="0" cellpadding="0" style="margin-left:auto;margin-right:auto">
+        <tr>
+            <td height="25px" style="height:25px"></td>
+        </tr>
+        <tr>
+            <td style="border-collapse:collapse;border-radius:2px;text-align:center;display:block;border:solid 1px #395fa0;padding:11px 20px 11px 20px">
+                <a href="http://pr.org/7" style="text-decoration:none;display:block" target="_blank">
+                    <center>
+                        <font size="3">
+                            <span style="font-family:Helvetica,Arial,sans-serif;font-weight:700;font-size:15px;line-height:14px;color:#395fa0;white-space:nowrap;vertical-align:middle">View Pull Request</span>
+                        </font>
+                    </center>
+                </a>
+            </td>
+        </tr>
+    </table>
+</center>
+        </td>
+    </tr>
+</table>
+                    </td>
+                    <td></td>
+                </tr>
+                <tr>
+                    <td height="30px" style="height:30px" colspan="3"></td>
+                </tr>
+            </table>
+        </td>
+        <td width="30px" style="width:30px"></td>
+    </tr>
+</table>
+<!--/body-->
+<!--/html-->
+<hr/>
+<hr/>
+<h1>pull_request, is_mention=True</h1>
+<pre>
+From: u1 u1 <name@example.com>
 To: u2@example.com
 Subject: [Review] repo/name PR #7 "The Title" from devbranch by u2
 </pre>
--- a/kallithea/tests/models/test_notifications.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/models/test_notifications.py	Wed Jun 17 21:52:16 2020 +0200
@@ -103,6 +103,7 @@
                             status_change=[None, 'Approved'],
                             cs_target_repo='http://example.com/repo_target',
                             cs_url='http://changeset.com',
+                            cs_author_username=User.get(self.u2).username,
                             cs_author=User.get(self.u2))),
                         (NotificationModel.TYPE_MESSAGE,
                          'This is the \'body\' of the "test" message\n - nothing interesting here except indentation.',
--- a/kallithea/tests/models/test_permissions.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/models/test_permissions.py	Wed Jun 17 21:52:16 2020 +0200
@@ -290,7 +290,7 @@
                               'hg.register.manual_activate',
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
-                              'usergroup.read', 'hg.create.write_on_repogroup.true'])
+                              'usergroup.read'])
 
     def test_inherit_sad_permissions_from_default_user(self):
         user_model = UserModel()
@@ -307,7 +307,7 @@
                               'hg.register.manual_activate',
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
-                              'usergroup.read', 'hg.create.write_on_repogroup.true'])
+                              'usergroup.read'])
 
     def test_inherit_more_permissions_from_default_user(self):
         user_model = UserModel()
@@ -333,7 +333,7 @@
                               'hg.register.manual_activate',
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
-                              'usergroup.read', 'hg.create.write_on_repogroup.true'])
+                              'usergroup.read'])
 
     def test_inherit_less_permissions_from_default_user(self):
         user_model = UserModel()
@@ -359,7 +359,7 @@
                               'hg.register.manual_activate',
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
-                              'usergroup.read', 'hg.create.write_on_repogroup.true'])
+                              'usergroup.read'])
 
     def test_inactive_user_group_does_not_affect_global_permissions(self):
         # Add user to inactive user group, set specific permissions on user
@@ -391,7 +391,7 @@
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
                               'usergroup.read',
-                              'hg.create.write_on_repogroup.true'])
+                              ])
 
     def test_inactive_user_group_does_not_affect_global_permissions_inverse(self):
         # Add user to inactive user group, set specific permissions on user
@@ -423,7 +423,7 @@
                               'hg.extern_activate.auto',
                               'repository.read', 'group.read',
                               'usergroup.read',
-                              'hg.create.write_on_repogroup.true'])
+                              ])
 
     def test_inactive_user_group_does_not_affect_repo_permissions(self):
         self.ug1 = fixture.create_user_group('G1')
--- a/kallithea/tests/other/test_libs.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/other/test_libs.py	Wed Jun 17 21:52:16 2020 +0200
@@ -119,12 +119,10 @@
                            ('F', False),
                            ('FALSE', False),
                            ('0', False),
-                           ('-1', False),
-                           ('', False)
     ])
-    def test_str2bool(self, str_bool, expected):
-        from kallithea.lib.utils2 import str2bool
-        assert str2bool(str_bool) == expected
+    def test_asbool(self, str_bool, expected):
+        from kallithea.lib.utils2 import asbool
+        assert asbool(str_bool) == expected
 
     def test_mention_extractor(self):
         from kallithea.lib.utils2 import extract_mentioned_usernames
--- a/kallithea/tests/scripts/manual_test_concurrency.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/kallithea/tests/scripts/manual_test_concurrency.py	Wed Jun 17 21:52:16 2020 +0200
@@ -37,7 +37,7 @@
 from paste.deploy import appconfig
 from sqlalchemy import engine_from_config
 
-from kallithea.config.environment import load_environment
+import kallithea.config.application
 from kallithea.lib.auth import get_crypt_password
 from kallithea.model import meta
 from kallithea.model.base import init_model
@@ -47,7 +47,7 @@
 
 rel_path = dirname(dirname(dirname(dirname(os.path.abspath(__file__)))))
 conf = appconfig('config:development.ini', relative_to=rel_path)
-load_environment(conf.global_conf, conf.local_conf)
+kallithea.config.application.make_app(conf.global_conf, **conf.local_conf)
 
 USER = TEST_USER_ADMIN_LOGIN
 PASS = TEST_USER_ADMIN_PASS
--- a/setup.py	Thu Jun 11 20:50:26 2020 +0200
+++ b/setup.py	Wed Jun 17 21:52:16 2020 +0200
@@ -53,7 +53,7 @@
     "FormEncode >= 1.3.1, < 1.4",
     "SQLAlchemy >= 1.2.9, < 1.4",
     "Mako >= 0.9.1, < 1.2",
-    "Pygments >= 2.2.0, < 2.6",
+    "Pygments >= 2.2.0, < 2.7",
     "Whoosh >= 2.7.1, < 2.8",
     "celery >= 4.3, < 4.5, != 4.4.4", # 4.4.4 is broken due to unexpressed dependency on 'future', see https://github.com/celery/celery/pull/6146
     "Babel >= 1.3, < 2.9",
@@ -65,7 +65,7 @@
     "dulwich >= 0.19.0, < 0.20",
     "mercurial >= 5.2, < 5.5",
     "decorator >= 4.2.1, < 4.5",
-    "Paste >= 2.0.3, < 3.4",
+    "Paste >= 2.0.3, < 3.5",
     "bleach >= 3.0, < 3.1.4",
     "Click >= 7.0, < 8",
     "ipaddr >= 2.2.0, < 2.3",
@@ -156,6 +156,6 @@
     kallithea-cli =    kallithea.bin.kallithea_cli:cli
 
     [paste.app_factory]
-    main = kallithea.config.middleware:make_app
+    main = kallithea.config.application:make_app
     """,
 )