changeset 8602:3e9d079fcf91

merge stable
author Thomas De Schampheleire <thomas.de_schampheleire@nokia.com>
date Sat, 22 Aug 2020 20:53:43 +0200
parents 1dbe842f32c4 (diff) eca0cb56a822 (current diff)
children b42eae7b7abb
files kallithea/lib/vcs/backends/hg/repository.py kallithea/lib/vcs/utils/helpers.py kallithea/tests/functional/test_admin_repos.py
diffstat 75 files changed, 1026 insertions(+), 883 deletions(-) [+]
line wrap: on
line diff
--- a/dev_requirements.txt	Sun Jul 26 00:03:12 2020 +0200
+++ b/dev_requirements.txt	Sat Aug 22 20:53:43 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
+isort == 5.1.2
+pyflakes == 2.2.0
--- a/development.ini	Sun Jul 26 00:03:12 2020 +0200
+++ b/development.ini	Sat Aug 22 20:53:43 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://kallithea:password@localhost/kallithea
+#sqlalchemy.url = mysql://kallithea:password@localhost/kallithea?charset=utf8mb4
+## Note: the mysql:// prefix should also be used for MariaDB
 
 sqlalchemy.pool_recycle = 3600
 
--- a/docs/contributing.rst	Sun Jul 26 00:03:12 2020 +0200
+++ b/docs/contributing.rst	Sat Aug 22 20:53:43 2020 +0200
@@ -92,6 +92,17 @@
 and the test suite creates repositories in the temporary directory. Linux
 systems with /tmp mounted noexec will thus fail.
 
+Tests can be run on PostgreSQL like::
+
+    sudo -u postgres createuser 'kallithea-test' --pwprompt  # password password
+    sudo -u postgres createdb 'kallithea-test' --owner 'kallithea-test'
+    REUSE_TEST_DB='postgresql://kallithea-test:password@localhost/kallithea-test' py.test
+
+Tests can be run on MariaDB/MySQL like::
+
+    echo "GRANT ALL PRIVILEGES ON \`kallithea-test\`.* TO 'kallithea-test'@'localhost' IDENTIFIED BY 'password'" | sudo -u mysql mysql
+    TEST_DB='mysql://kallithea-test:password@localhost/kallithea-test?charset=utf8mb4' py.test
+
 You can also use ``tox`` to run the tests with all supported Python versions.
 
 When running tests, Kallithea generates a `test.ini` based on template values
--- a/docs/overview.rst	Sun Jul 26 00:03:12 2020 +0200
+++ b/docs/overview.rst	Sat Aug 22 20:53:43 2020 +0200
@@ -30,13 +30,17 @@
     database schema and insert the most basic information: the location of the
     repository store and an initial local admin user.
 
-5. **Configure the web server.**
+5. **Prepare front-end files**
+    Some front-end files must be fetched or created using ``npm`` tooling so
+    they can be served to the client as static files.
+
+6. **Configure the web server.**
     The web server must invoke the WSGI entrypoint for the Kallithea software
     using the ``.ini`` file (and thus the database). This makes the web
     application available so the local admin user can log in and tweak the
     configuration further.
 
-6. **Configure users.**
+7. **Configure users.**
     The initial admin user can create additional local users, or configure how
     users can be created and authenticated from other user directories.
 
@@ -177,7 +181,7 @@
   to get a configuration starting point for your choice of web server.
 
   (Gearbox will do like ``paste`` and use the WSGI application entry point
-  ``kallithea.config.middleware:make_app`` as specified in ``setup.py``.)
+  ``kallithea.config.application:make_app`` as specified in ``setup.py``.)
 
 - `Apache httpd`_ can serve WSGI applications directly using mod_wsgi_ and a
   simple Python file with the necessary configuration. This is a good option if
--- a/docs/setup.rst	Sun Jul 26 00:03:12 2020 +0200
+++ b/docs/setup.rst	Sat Aug 22 20:53:43 2020 +0200
@@ -8,32 +8,68 @@
 Setting up Kallithea
 --------------------
 
-First, you will need to create a Kallithea configuration file. Run the
-following command to do so::
+Some further details to the steps mentioned in the overview.
+
+Create low level configuration file
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 
-    kallithea-cli config-create my.ini
+First, you will need to create a Kallithea configuration file. The
+configuration file is a ``.ini`` file that contains various low level settings
+for Kallithea, e.g. configuration of how to use database, web server, email,
+and logging.
 
-This will create the file ``my.ini`` in the current directory. This
-configuration file contains the various settings for Kallithea, e.g.
-proxy port, email settings, usage of static files, cache, Celery
-settings, and logging. Extra settings can be specified like::
+Run the following command to create the file ``my.ini`` in the current
+directory::
+
+    kallithea-cli config-create my.ini http_server=waitress
+
+To get a good starting point for your configuration, specify the http server
+you intend to use. It can be ``waitress``, ``gearbox``, ``gevent``,
+``gunicorn``, or ``uwsgi``. (Apache ``mod_wsgi`` will not use this
+configuration file, and it is fine to keep the default http_server configuration
+unused. ``mod_wsgi`` is configured using ``httpd.conf`` directives and a WSGI
+wrapper script.)
+
+Extra custom settings can be specified like::
 
     kallithea-cli config-create my.ini host=8.8.8.8 "[handler_console]" formatter=color_formatter
 
-Next, you need to create the databases used by Kallithea. It is recommended to
-use PostgreSQL or SQLite (default). If you choose a database other than the
-default, ensure you properly adjust the database URL in your ``my.ini``
-configuration file to use this other database. Kallithea currently supports
-PostgreSQL, SQLite and MariaDB/MySQL databases. Create the database by running
-the following command::
+Populate the database
+^^^^^^^^^^^^^^^^^^^^^
+
+Next, you need to create the databases used by Kallithea. Kallithea currently
+supports PostgreSQL, SQLite and MariaDB/MySQL databases. It is recommended to
+start out using SQLite (the default) and move to PostgreSQL if it becomes a
+bottleneck or to get a "proper" database. MariaDB/MySQL is also supported.
+
+For PostgreSQL, run ``pip install psycopg2`` to get the database driver. Make
+sure the PostgreSQL server is initialized and running. Make sure you have a
+database user with password authentication with permissions to create databases
+- for example by running::
+
+    sudo -u postgres createuser 'kallithea' --pwprompt --createdb
+
+For MariaDB/MySQL, run ``pip install mysqlclient`` to get the ``MySQLdb``
+database driver. Make sure the database server is initialized and running. Make
+sure you have a database user with password authentication with permissions to
+create the database - for example by running::
+
+    echo 'CREATE USER "kallithea"@"localhost" IDENTIFIED BY "password"' | sudo -u mysql mysql
+    echo 'GRANT ALL PRIVILEGES ON `kallithea`.* TO "kallithea"@"localhost"' | sudo -u mysql mysql
+
+Check and adjust ``sqlalchemy.url`` in your ``my.ini`` configuration file to use
+this database.
+
+Create the database, tables, and initial content by running the following
+command::
 
     kallithea-cli db-create -c my.ini
 
-This will prompt you for a "root" path. This "root" path is the location where
-Kallithea will store all of its repositories on the current machine. After
-entering this "root" path ``db-create`` will also prompt you for a username
-and password for the initial admin account which ``db-create`` sets
-up for you.
+This will first prompt you for a "root" path. This "root" path is the location
+where Kallithea will store all of its repositories on the current machine. This
+location must be writable for the running Kallithea application. Next,
+``db-create`` will prompt you for a username and password for the initial admin
+account it sets up for you.
 
 The ``db-create`` values can also be given on the command line.
 Example::
@@ -48,11 +84,17 @@
 location to its database.  (Note: make sure you specify the correct
 path to the root).
 
-.. note:: the given path for Mercurial_ repositories **must** be write
-          accessible for the application. It's very important since
-          the Kallithea web interface will work without write access,
-          but when trying to do a push it will fail with permission
-          denied errors unless it has write access.
+.. note:: It is also possible to use an existing database. For example,
+          when using PostgreSQL without granting general createdb privileges to
+          the PostgreSQL kallithea user, set ``sqlalchemy.url =
+          postgresql://kallithea:password@localhost/kallithea`` and create the
+          database like::
+
+              sudo -u postgres createdb 'kallithea' --owner 'kallithea'
+              kallithea-cli db-create -c my.ini --reuse
+
+Prepare front-end files
+^^^^^^^^^^^^^^^^^^^^^^^
 
 Finally, the front-end files must be prepared. This requires ``npm`` version 6
 or later, which needs ``node.js`` (version 12 or later). Prepare the front-end
@@ -60,7 +102,11 @@
 
     kallithea-cli front-end-build
 
-You are now ready to use Kallithea. To run it simply execute::
+Running
+^^^^^^^
+
+You are now ready to use Kallithea. To run it using a gearbox web server,
+simply execute::
 
     gearbox serve -c my.ini
 
--- a/kallithea/__init__.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/__init__.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/bin/kallithea_cli_base.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/bin/kallithea_cli_db.py	Sat Aug 22 20:53:43 2020 +0200
@@ -20,6 +20,8 @@
 
 
 @cli_base.register_command(config_file=True)
+@click.option('--reuse/--no-reuse', default=False,
+        help='Reuse and clean existing database instead of dropping and creating (default: no reuse)')
 @click.option('--user', help='Username of administrator account.')
 @click.option('--password', help='Password for administrator account.')
 @click.option('--email', help='Email address of administrator account.')
@@ -28,7 +30,7 @@
 @click.option('--force-no', is_flag=True, help='Answer no to every question.')
 @click.option('--public-access/--no-public-access', default=True,
         help='Enable/disable public access on this installation (default: enable)')
-def db_create(user, password, email, repos, force_yes, force_no, public_access):
+def db_create(user, password, email, repos, force_yes, force_no, public_access, reuse):
     """Initialize the database.
 
     Create all required tables in the database specified in the configuration
@@ -57,7 +59,7 @@
     )
     dbmanage = DbManage(dbconf=dbconf, root=kallithea.CONFIG['here'],
                         tests=False, cli_args=cli_args)
-    dbmanage.create_tables(override=True)
+    dbmanage.create_tables(reuse_database=reuse)
     repo_root_path = dbmanage.prompt_repo_root_path(None)
     dbmanage.create_settings(repo_root_path)
     dbmanage.create_default_user()
@@ -67,7 +69,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/bin/kallithea_cli_ssh.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/config/app_cfg.py	Sat Aug 22 20:53:43 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)
@@ -111,8 +86,8 @@
 # 'debug = true' (not in production!)
 # See the Kallithea documentation for more information.
 try:
+    import kajiki  # only to check its existence
     from tgext.debugbar import enable_debugbar
-    import kajiki # only to check its existence
     assert kajiki
 except ImportError:
     pass
@@ -134,7 +109,7 @@
         mercurial.encoding.encoding = hgencoding
 
     if config.get('ignore_alembic_revision', False):
-        log.warn('database alembic revision checking is disabled')
+        log.warning('database alembic revision checking is disabled')
     else:
         dbconf = config['sqlalchemy.url']
         alembic_cfg = alembic.config.Config()
@@ -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	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 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	Sun Jul 26 00:03:12 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/admin/permissions.py	Sat Aug 22 20:53:43 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,8 +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],
                 [x[0] for x in c.register_choices],
@@ -157,15 +151,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/admin/repo_groups.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/admin/repos.py	Sat Aug 22 20:53:43 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/changeset.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/changeset.py	Sat Aug 22 20:53:43 2020 +0200
@@ -28,7 +28,7 @@
 import binascii
 import logging
 import traceback
-from collections import OrderedDict, defaultdict
+from collections import OrderedDict
 
 from tg import request, response
 from tg import tmpl_context as c
@@ -54,118 +54,6 @@
 log = logging.getLogger(__name__)
 
 
-def _update_with_GET(params, GET):
-    for k in ['diff1', 'diff2', 'diff']:
-        params[k] += GET.getall(k)
-
-
-def anchor_url(revision, path, GET):
-    fid = h.FID(revision, path)
-    return h.url.current(anchor=fid, **dict(GET))
-
-
-def get_ignore_ws(fid, GET):
-    ig_ws_global = GET.get('ignorews')
-    ig_ws = [k for k in GET.getall(fid) if k.startswith('WS')]
-    if ig_ws:
-        try:
-            return int(ig_ws[0].split(':')[-1])
-        except ValueError:
-            raise HTTPBadRequest()
-    return ig_ws_global
-
-
-def _ignorews_url(GET, fileid=None):
-    fileid = str(fileid) if fileid else None
-    params = defaultdict(list)
-    _update_with_GET(params, GET)
-    lbl = _('Show whitespace')
-    ig_ws = get_ignore_ws(fileid, GET)
-    ln_ctx = get_line_ctx(fileid, GET)
-    # global option
-    if fileid is None:
-        if ig_ws is None:
-            params['ignorews'] += [1]
-            lbl = _('Ignore whitespace')
-        ctx_key = 'context'
-        ctx_val = ln_ctx
-    # per file options
-    else:
-        if ig_ws is None:
-            params[fileid] += ['WS:1']
-            lbl = _('Ignore whitespace')
-
-        ctx_key = fileid
-        ctx_val = 'C:%s' % ln_ctx
-    # if we have passed in ln_ctx pass it along to our params
-    if ln_ctx:
-        params[ctx_key] += [ctx_val]
-
-    params['anchor'] = fileid
-    icon = h.literal('<i class="icon-strike"></i>')
-    return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
-
-
-def get_line_ctx(fid, GET):
-    ln_ctx_global = GET.get('context')
-    if fid:
-        ln_ctx = [k for k in GET.getall(fid) if k.startswith('C')]
-    else:
-        _ln_ctx = [k for k in GET if k.startswith('C')]
-        ln_ctx = GET.get(_ln_ctx[0]) if _ln_ctx else ln_ctx_global
-        if ln_ctx:
-            ln_ctx = [ln_ctx]
-
-    if ln_ctx:
-        retval = ln_ctx[0].split(':')[-1]
-    else:
-        retval = ln_ctx_global
-
-    try:
-        return int(retval)
-    except Exception:
-        return 3
-
-
-def _context_url(GET, fileid=None):
-    """
-    Generates url for context lines
-
-    :param fileid:
-    """
-
-    fileid = str(fileid) if fileid else None
-    ig_ws = get_ignore_ws(fileid, GET)
-    ln_ctx = (get_line_ctx(fileid, GET) or 3) * 2
-
-    params = defaultdict(list)
-    _update_with_GET(params, GET)
-
-    # global option
-    if fileid is None:
-        if ln_ctx > 0:
-            params['context'] += [ln_ctx]
-
-        if ig_ws:
-            ig_ws_key = 'ignorews'
-            ig_ws_val = 1
-
-    # per file option
-    else:
-        params[fileid] += ['C:%s' % ln_ctx]
-        ig_ws_key = fileid
-        ig_ws_val = 'WS:%s' % 1
-
-    if ig_ws:
-        params[ig_ws_key] += [ig_ws_val]
-
-    lbl = _('Increase diff context to %(num)s lines') % {'num': ln_ctx}
-
-    params['anchor'] = fileid
-    icon = h.literal('<i class="icon-sort"></i>')
-    return h.link_to(icon, h.url.current(**params), title=lbl, **{'data-toggle': 'tooltip'})
-
-
 def create_cs_pr_comment(repo_name, revision=None, pull_request=None, allowed_to_change_status=True):
     """
     Add a comment to the specified changeset or pull request, using POST values
@@ -292,9 +180,6 @@
 
     def _index(self, revision, method):
         c.pull_request = None
-        c.anchor_url = anchor_url
-        c.ignorews_url = _ignorews_url
-        c.context_url = _context_url
         c.fulldiff = request.GET.get('fulldiff') # for reporting number of changed files
         # get ranges of revisions if preset
         rev_range = revision.split('...')[:2]
@@ -357,11 +242,10 @@
 
             cs2 = changeset.raw_id
             cs1 = changeset.parents[0].raw_id if changeset.parents else EmptyChangeset().raw_id
-            context_lcl = get_line_ctx('', request.GET)
-            ign_whitespace_lcl = get_ignore_ws('', request.GET)
-
+            ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET)
+            diff_context_size = h.get_diff_context_size(request.GET)
             raw_diff = diffs.get_diff(c.db_repo_scm_instance, cs1, cs2,
-                ignore_whitespace=ign_whitespace_lcl, context=context_lcl)
+                ignore_whitespace=ignore_whitespace_diff, context=diff_context_size)
             diff_limit = None if c.fulldiff else self.cut_off_limit
             file_diff_data = []
             if method == 'show':
--- a/kallithea/controllers/compare.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/compare.py	Sat Aug 22 20:53:43 2020 +0200
@@ -37,13 +37,12 @@
 from webob.exc import HTTPBadRequest, HTTPFound, HTTPNotFound
 
 from kallithea.config.routing import url
-from kallithea.controllers.changeset import _context_url, _ignorews_url
 from kallithea.lib import diffs
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 from kallithea.lib.base import BaseRepoController, render
 from kallithea.lib.graphmod import graph_data
-from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes, safe_int
+from kallithea.lib.utils2 import ascii_bytes, ascii_str, safe_bytes
 from kallithea.model.db import Repository
 
 
@@ -131,8 +130,8 @@
 
         elif alias == 'git':
             if org_repo != other_repo:
+                from dulwich.client import SubprocessGitClient
                 from dulwich.repo import Repo
-                from dulwich.client import SubprocessGitClient
 
                 gitrepo = Repo(org_repo.path)
                 SubprocessGitClient(thin_packs=False).fetch(other_repo.path, gitrepo)
@@ -208,12 +207,8 @@
             other_repo=c.a_repo.repo_name,
             other_ref_type=org_ref_type, other_ref_name=org_ref_name,
             merge=merge or '')
-
-        # set callbacks for generating markup for icons
-        c.ignorews_url = _ignorews_url
-        c.context_url = _context_url
-        ignore_whitespace = request.GET.get('ignorews') == '1'
-        line_context = safe_int(request.GET.get('context'), 3)
+        ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET)
+        diff_context_size = h.get_diff_context_size(request.GET)
 
         c.a_rev = self._get_ref_rev(c.a_repo, org_ref_type, org_ref_name,
             returnempty=True)
@@ -275,8 +270,8 @@
         log.debug('running diff between %s and %s in %s',
                   rev1, c.cs_rev, org_repo.scm_instance.path)
         raw_diff = diffs.get_diff(org_repo.scm_instance, rev1=rev1, rev2=c.cs_rev,
-                                      ignore_whitespace=ignore_whitespace,
-                                      context=line_context)
+                                      ignore_whitespace=ignore_whitespace_diff,
+                                      context=diff_context_size)
 
         diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit)
         c.limited_diff = diff_processor.limited_diff
--- a/kallithea/controllers/feed.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/feed.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/files.py	Sat Aug 22 20:53:43 2020 +0200
@@ -39,14 +39,13 @@
 from webob.exc import HTTPFound, HTTPNotFound
 
 from kallithea.config.routing import url
-from kallithea.controllers.changeset import _context_url, _ignorews_url, anchor_url, get_ignore_ws, get_line_ctx
 from kallithea.lib import diffs
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
 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_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,
@@ -558,8 +557,8 @@
     @LoginRequired(allow_default_user=True)
     @HasRepoPermissionLevelDecorator('read')
     def diff(self, repo_name, f_path):
-        ignore_whitespace = request.GET.get('ignorews') == '1'
-        line_context = safe_int(request.GET.get('context'), 3)
+        ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET)
+        diff_context_size = h.get_diff_context_size(request.GET)
         diff2 = request.GET.get('diff2', '')
         diff1 = request.GET.get('diff1', '') or diff2
         c.action = request.GET.get('diff')
@@ -567,9 +566,6 @@
         c.f_path = f_path
         c.big_diff = False
         fulldiff = request.GET.get('fulldiff')
-        c.anchor_url = anchor_url
-        c.ignorews_url = _ignorews_url
-        c.context_url = _context_url
         c.changes = OrderedDict()
         c.changes[diff2] = []
 
@@ -577,7 +573,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:
@@ -624,8 +620,8 @@
 
         if c.action == 'download':
             raw_diff = diffs.get_gitdiff(node1, node2,
-                                      ignore_whitespace=ignore_whitespace,
-                                      context=line_context)
+                                      ignore_whitespace=ignore_whitespace_diff,
+                                      context=diff_context_size)
             diff_name = '%s_vs_%s.diff' % (diff1, diff2)
             response.content_type = 'text/plain'
             response.content_disposition = (
@@ -635,25 +631,21 @@
 
         elif c.action == 'raw':
             raw_diff = diffs.get_gitdiff(node1, node2,
-                                      ignore_whitespace=ignore_whitespace,
-                                      context=line_context)
+                                      ignore_whitespace=ignore_whitespace_diff,
+                                      context=diff_context_size)
             response.content_type = 'text/plain'
             return raw_diff
 
         else:
             fid = h.FID(diff2, node2.path)
-            line_context_lcl = get_line_ctx(fid, request.GET)
-            ign_whitespace_lcl = get_ignore_ws(fid, request.GET)
-
             diff_limit = None if fulldiff else self.cut_off_limit
             c.a_rev, c.cs_rev, a_path, diff, st, op = diffs.wrapped_diff(filenode_old=node1,
                                          filenode_new=node2,
                                          diff_limit=diff_limit,
-                                         ignore_whitespace=ign_whitespace_lcl,
-                                         line_context=line_context_lcl,
+                                         ignore_whitespace=ignore_whitespace_diff,
+                                         line_context=diff_context_size,
                                          enable_comments=False)
             c.file_diff_data = [(fid, fid, op, a_path, node2.path, diff, st)]
-
             return render('files/file_diff.html')
 
     @LoginRequired(allow_default_user=True)
--- a/kallithea/controllers/forks.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/forks.py	Sat Aug 22 20:53:43 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/controllers/pullrequests.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/controllers/pullrequests.py	Sat Aug 22 20:53:43 2020 +0200
@@ -36,7 +36,7 @@
 from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPFound, HTTPNotFound
 
 from kallithea.config.routing import url
-from kallithea.controllers.changeset import _context_url, _ignorews_url, create_cs_pr_comment, delete_cs_pr_comment
+from kallithea.controllers.changeset import create_cs_pr_comment, delete_cs_pr_comment
 from kallithea.lib import diffs
 from kallithea.lib import helpers as h
 from kallithea.lib.auth import HasRepoPermissionLevelDecorator, LoginRequired
@@ -569,10 +569,8 @@
         c.cs_comments = c.cs_repo.get_comments(raw_ids)
         c.cs_statuses = c.cs_repo.statuses(raw_ids)
 
-        ignore_whitespace = request.GET.get('ignorews') == '1'
-        line_context = safe_int(request.GET.get('context'), 3)
-        c.ignorews_url = _ignorews_url
-        c.context_url = _context_url
+        ignore_whitespace_diff = h.get_ignore_whitespace_diff(request.GET)
+        diff_context_size = h.get_diff_context_size(request.GET)
         fulldiff = request.GET.get('fulldiff')
         diff_limit = None if fulldiff else self.cut_off_limit
 
@@ -581,7 +579,7 @@
                   c.a_rev, c.cs_rev, org_scm_instance.path)
         try:
             raw_diff = diffs.get_diff(org_scm_instance, rev1=c.a_rev, rev2=c.cs_rev,
-                                      ignore_whitespace=ignore_whitespace, context=line_context)
+                                      ignore_whitespace=ignore_whitespace_diff, context=diff_context_size)
         except ChangesetDoesNotExistError:
             raw_diff = safe_bytes(_("The diff can't be shown - the PR revisions could not be found."))
         diff_processor = diffs.DiffProcessor(raw_diff, diff_limit=diff_limit)
--- a/kallithea/front-end/kallithea-diff.less	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/front-end/kallithea-diff.less	Sat Aug 22 20:53:43 2020 +0200
@@ -62,6 +62,7 @@
   border-collapse: collapse;
   border-radius: 0px !important;
   width: 100%;
+  table-layout: fixed;
 
   /* line coloring */
   .context {
@@ -105,31 +106,26 @@
     border-color: rgba(0, 0, 0, 0.3);
   }
 
-  /* line numbers */
-  .lineno {
-    padding-left: 2px;
-    padding-right: 2px !important;
-    width: 30px;
+  /* line number columns */
+  td.lineno {
+    width: 4em;
     border-right: 1px solid @panel-default-border !important;
     vertical-align: middle !important;
-    text-align: center;
-  }
-  .lineno.new {
-    text-align: right;
-  }
-  .lineno.old {
-    text-align: right;
-  }
-  .lineno a {
-    color: #aaa !important;
     font-size: 11px;
     font-family: @font-family-monospace;
     line-height: normal;
-    padding-left: 6px;
-    padding-right: 6px;
-    display: block;
+    text-align: center;
+  }
+  td.lineno[colspan="2"] {
+    width: 8em;
   }
-  .line:hover .lineno a {
+  td.lineno a {
+    color: #aaa !important;
+    display: inline-block;
+    min-width: 2em;
+    text-align: right;
+  }
+  tr.line:hover td.lineno a {
     color: #333 !important;
   }
   /** CODE **/
@@ -172,10 +168,7 @@
   left: -8px;
   box-sizing: border-box;
 }
-/* comment bubble, only visible when in a commentable diff */
-.commentable-diff tr.line.add:hover td .add-bubble,
-.commentable-diff tr.line.del:hover td .add-bubble,
-.commentable-diff tr.line.unmod:hover td .add-bubble {
+.commentable-diff tr.line:hover td .add-bubble {
   display: block;
   z-index: 1;
 }
--- a/kallithea/lib/auth.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/auth.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/auth_modules/__init__.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/auth_modules/auth_container.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/base.py	Sat Aug 22 20:53:43 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/__init__.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/celerylib/__init__.py	Sat Aug 22 20:53:43 2020 +0200
@@ -28,7 +28,7 @@
 
 import logging
 import os
-from hashlib import md5
+from hashlib import sha1
 
 from decorator import decorator
 from tg import config
@@ -94,7 +94,7 @@
     func_name = str(func.__name__) if hasattr(func, '__name__') else str(func)
 
     lockkey = 'task_%s.lock' % \
-        md5(safe_bytes(func_name + '-' + '-'.join(str(x) for x in params))).hexdigest()
+        sha1(safe_bytes(func_name + '-' + '-'.join(str(x) for x in params))).hexdigest()
     return lockkey
 
 
--- a/kallithea/lib/celerylib/tasks.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/celerylib/tasks.py	Sat Aug 22 20:53:43 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"
@@ -323,8 +323,8 @@
 @celerylib.task
 @celerylib.dbsession
 def create_repo(form_data, cur_user):
+    from kallithea.model.db import Setting
     from kallithea.model.repo import RepoModel
-    from kallithea.model.db import Setting
 
     DBS = celerylib.get_session()
 
--- a/kallithea/lib/colored_formatter.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/colored_formatter.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/db_manage.py	Sat Aug 22 20:53:43 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
 
 
@@ -74,46 +72,48 @@
             init_model(engine)
             self.sa = Session()
 
-    def create_tables(self, override=False):
-        """
-        Create a auth database
+    def create_tables(self, reuse_database=False):
         """
-
-        log.info("Any existing database is going to be destroyed")
-        if self.tests:
-            destroy = True
+        Create database (optional) and tables.
+        If reuse_database is false, the database will be dropped (if it exists)
+        and a new one created. If true, the existing database will be reused
+        and cleaned for content.
+        """
+        url = sqlalchemy.engine.url.make_url(self.dburi)
+        database = url.database
+        if reuse_database:
+            log.info("The content of the database %r will be destroyed and new tables created." % database)
         else:
-            destroy = self._ask_ok('Are you sure to destroy old database ? [y/n]')
-        if not destroy:
-            print('Nothing done.')
-            sys.exit(0)
-        if destroy:
-            # drop and re-create old schemas
+            log.info("The existing database %r will be destroyed and a new one created." % database)
 
-            url = sqlalchemy.engine.url.make_url(self.dburi)
-            database = url.database
+        if not self.tests:
+            if not self._ask_ok('Are you sure to destroy old database? [y/n]'):
+                print('Nothing done.')
+                sys.exit(0)
 
-            # Some databases enforce foreign key constraints and Base.metadata.drop_all() doesn't work
+        if reuse_database:
+            Base.metadata.drop_all()
+        else:
             if url.drivername == 'mysql':
                 url.database = None  # don't connect to the database (it might not exist)
                 engine = sqlalchemy.create_engine(url)
                 with engine.connect() as conn:
-                    conn.execute('DROP DATABASE IF EXISTS ' + database)
-                    conn.execute('CREATE DATABASE ' + database)
+                    conn.execute('DROP DATABASE IF EXISTS `%s`' % database)
+                    conn.execute('CREATE DATABASE `%s` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci' % database)
             elif url.drivername == 'postgresql':
                 from psycopg2.extensions import ISOLATION_LEVEL_AUTOCOMMIT
                 url.database = 'postgres'  # connect to the system database (as the real one might not exist)
                 engine = sqlalchemy.create_engine(url)
                 with engine.connect() as conn:
                     conn.connection.set_isolation_level(ISOLATION_LEVEL_AUTOCOMMIT)
-                    conn.execute('DROP DATABASE IF EXISTS ' + database)
-                    conn.execute('CREATE DATABASE ' + database)
+                    conn.execute('DROP DATABASE IF EXISTS "%s"' % database)
+                    conn.execute('CREATE DATABASE "%s"' % database)
             else:
+                # Some databases enforce foreign key constraints and Base.metadata.drop_all() doesn't work, but this is
                 # known to work on SQLite - possibly not on other databases with strong referential integrity
                 Base.metadata.drop_all()
 
-        checkfirst = not override
-        Base.metadata.create_all(checkfirst=checkfirst)
+        Base.metadata.create_all(checkfirst=False)
 
         # Create an Alembic configuration and generate the version table,
         # "stamping" it with the most recent Alembic migration revision, to
@@ -128,42 +128,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
@@ -199,11 +163,9 @@
             self.create_user(username, password, email, True)
         else:
             log.info('creating admin and regular test users')
-            from kallithea.tests.base import TEST_USER_ADMIN_LOGIN, \
-                TEST_USER_ADMIN_PASS, TEST_USER_ADMIN_EMAIL, \
-                TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, \
-                TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR2_LOGIN, \
-                TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR2_EMAIL
+            from kallithea.tests.base import (TEST_USER_ADMIN_EMAIL, TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS, TEST_USER_REGULAR2_EMAIL,
+                                              TEST_USER_REGULAR2_LOGIN, TEST_USER_REGULAR2_PASS, TEST_USER_REGULAR_EMAIL, TEST_USER_REGULAR_LOGIN,
+                                              TEST_USER_REGULAR_PASS)
 
             self.create_user(TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS,
                              TEST_USER_ADMIN_EMAIL, True)
@@ -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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/diffs.py	Sat Aug 22 20:53:43 2020 +0200
@@ -70,80 +70,72 @@
     """
     Return given diff as html table with customized css classes
     """
-    def _link_to_if(condition, label, url):
-        """
-        Generates a link if condition is meet or just the label if not.
-        """
-
-        if condition:
-            return '''<a href="%(url)s" data-pseudo-content="%(label)s"></a>''' % {
-                'url': url,
-                'label': label
-            }
-        else:
-            return label
-
     _html_empty = True
     _html = []
     _html.append('''<table class="%(table_class)s">\n''' % {
         'table_class': table_class
     })
 
-    for diff in parsed_lines:
-        for line in diff['chunks']:
+    for file_info in parsed_lines:
+        count_no_lineno = 0  # counter to allow comments on lines without new/old line numbers
+        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']
                 })
-                anchor_old_id = ''
-                anchor_new_id = ''
-                anchor_old = "%(filename)s_o%(oldline_no)s" % {
-                    'filename': _safe_id(diff['filename']),
-                    'oldline_no': change['old_lineno']
-                }
-                anchor_new = "%(filename)s_n%(oldline_no)s" % {
-                    'filename': _safe_id(diff['filename']),
-                    'oldline_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'] == '...')
-                if cond_old:
-                    anchor_old_id = 'id="%s"' % anchor_old
-                if cond_new:
-                    anchor_new_id = 'id="%s"' % anchor_new
-                ###########################################################
-                # OLD LINE NUMBER
-                ###########################################################
-                _html.append('''\t<td %(a_id)s class="%(olc)s" %(colspan)s>''' % {
-                    'a_id': anchor_old_id,
-                    'olc': no_lineno_class if no_lineno else old_lineno_class,
-                    'colspan': 'colspan="2"' if no_lineno else ''
-                })
-
-                _html.append('''%(link)s''' % {
-                    'link': _link_to_if(not no_lineno, change['old_lineno'],
-                                        '#%s' % anchor_old)
-                })
-                _html.append('''</td>\n''')
-                ###########################################################
-                # NEW LINE NUMBER
-                ###########################################################
-
-                if not no_lineno:
+                if change['old_lineno'] or change['new_lineno']:
+                    ###########################################################
+                    # OLD LINE NUMBER
+                    ###########################################################
+                    anchor_old = "%(filename)s_o%(oldline_no)s" % {
+                        'filename': _safe_id(file_info['filename']),
+                        'oldline_no': change['old_lineno']
+                    }
+                    anchor_old_id = ''
+                    if change['old_lineno']:
+                        anchor_old_id = 'id="%s"' % anchor_old
+                    _html.append('''\t<td %(a_id)s class="%(olc)s">''' % {
+                        'a_id': anchor_old_id,
+                        'olc': old_lineno_class,
+                    })
+                    _html.append('''<a href="%(url)s" data-pseudo-content="%(label)s"></a>''' % {
+                        'label': change['old_lineno'],
+                        'url': '#%s' % anchor_old,
+                    })
+                    _html.append('''</td>\n''')
+                    ###########################################################
+                    # NEW LINE NUMBER
+                    ###########################################################
+                    anchor_new = "%(filename)s_n%(newline_no)s" % {
+                        'filename': _safe_id(file_info['filename']),
+                        'newline_no': change['new_lineno']
+                    }
+                    anchor_new_id = ''
+                    if change['new_lineno']:
+                        anchor_new_id = 'id="%s"' % anchor_new
                     _html.append('''\t<td %(a_id)s class="%(nlc)s">''' % {
                         'a_id': anchor_new_id,
                         'nlc': new_lineno_class
                     })
-
-                    _html.append('''%(link)s''' % {
-                        'link': _link_to_if(True, change['new_lineno'],
-                                            '#%s' % anchor_new)
+                    _html.append('''<a href="%(url)s" data-pseudo-content="%(label)s"></a>''' % {
+                        'label': change['new_lineno'],
+                        'url': '#%s' % anchor_new,
+                    })
+                    _html.append('''</td>\n''')
+                else:
+                    ###########################################################
+                    # NO LINE NUMBER
+                    ###########################################################
+                    anchor = "%(filename)s_%(count_no_lineno)s" % {
+                        'filename': _safe_id(file_info['filename']),
+                        'count_no_lineno': count_no_lineno,
+                    }
+                    count_no_lineno += 1
+                    _html.append('''\t<td id="%(anchor)s" class="%(olc)s" colspan="2">''' % {
+                        'anchor': anchor,
+                        'olc': no_lineno_class,
                     })
                     _html.append('''</td>\n''')
                 ###########################################################
@@ -453,7 +445,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 +462,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 +579,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 +624,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/helpers.py	Sat Aug 22 20:53:43 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
 #==============================================================================
@@ -213,12 +213,48 @@
     """
     Creates a unique ID for filenode based on it's hash of path and revision
     it's safe to use in urls
+    """
+    return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
 
-    :param raw_id:
-    :param path:
-    """
+
+def get_ignore_whitespace_diff(GET):
+    """Return true if URL requested whitespace to be ignored"""
+    return bool(GET.get('ignorews'))
+
 
-    return 'C-%s-%s' % (short_id(raw_id), hashlib.md5(safe_bytes(path)).hexdigest()[:12])
+def ignore_whitespace_link(GET, anchor=None):
+    """Return snippet with link to current URL with whitespace ignoring toggled"""
+    params = dict(GET)  # ignoring duplicates
+    if get_ignore_whitespace_diff(GET):
+        params.pop('ignorews')
+        title = _("Show whitespace changes")
+    else:
+        params['ignorews'] = '1'
+        title = _("Ignore whitespace changes")
+    params['anchor'] = anchor
+    return link_to(
+        literal('<i class="icon-strike"></i>'),
+        url.current(**params),
+        title=title,
+        **{'data-toggle': 'tooltip'})
+
+
+def get_diff_context_size(GET):
+    """Return effective context size requested in URL"""
+    return safe_int(GET.get('context'), default=3)
+
+
+def increase_context_link(GET, anchor=None):
+    """Return snippet with link to current URL with double context size"""
+    context = get_diff_context_size(GET) * 2
+    params = dict(GET)  # ignoring duplicates
+    params['context'] = str(context)
+    params['anchor'] = anchor
+    return link_to(
+        literal('<i class="icon-sort"></i>'),
+        url.current(**params),
+        title=_('Increase diff context to %(num)s lines') % {'num': context},
+        **{'data-toggle': 'tooltip'})
 
 
 class _FilesBreadCrumbs(object):
@@ -526,7 +562,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:
@@ -596,6 +632,7 @@
     """Find the user identified by 'author', return one of the users attributes,
     default to the username attribute, None if there is no user"""
     from kallithea.model.db import User
+
     # if author is already an instance use it for extraction
     if isinstance(author, User):
         return getattr(author, show_attr)
@@ -610,6 +647,7 @@
 
 def person_by_id(id_, show_attr="username"):
     from kallithea.model.db import User
+
     # maybe it's an ID ?
     if str(id_).isdigit() or isinstance(id_, int):
         id_ = int(id_)
@@ -932,16 +970,14 @@
     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)
 
 
 def gravatar_url(email_address, size=30, default=''):
-    # doh, we need to re-import those to mock it later
-    from kallithea.config.routing import url
-    from kallithea.model.db import User
     from tg import tmpl_context as c
+
     if not c.visual.use_gravatar:
         return ""
 
@@ -951,6 +987,10 @@
     if email_address == _def:
         return default
 
+    # re-import url so tests can mock it
+    from kallithea.config.routing import url
+    from kallithea.model.db import User
+
     parsed_url = urllib.parse.urlparse(url.current(qualified=True))
     url = (c.visual.gravatar_url or User.DEFAULT_GRAVATAR_URL) \
                .replace('{email}', email_address) \
@@ -986,8 +1026,7 @@
 
     :param stats: two element list of added/deleted lines of code
     """
-    from kallithea.lib.diffs import NEW_FILENODE, DEL_FILENODE, \
-        MOD_FILENODE, RENAMED_FILENODE, CHMOD_FILENODE, BIN_FILENODE
+    from kallithea.lib.diffs import BIN_FILENODE, CHMOD_FILENODE, DEL_FILENODE, MOD_FILENODE, NEW_FILENODE, RENAMED_FILENODE
 
     a, d = stats['added'], stats['deleted']
     width = 100
--- a/kallithea/lib/hooks.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/hooks.py	Sat Aug 22 20:53:43 2020 +0200
@@ -307,14 +307,15 @@
     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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/inifile.py	Sat Aug 22 20:53:43 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/markup_renderer.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/markup_renderer.py	Sat Aug 22 20:53:43 2020 +0200
@@ -74,13 +74,13 @@
 
         :param text:
         """
-        from hashlib import md5
+        from hashlib import sha1
 
         # Extract pre blocks.
         extractions = {}
 
         def pre_extraction_callback(matchobj):
-            digest = md5(matchobj.group(0)).hexdigest()
+            digest = sha1(matchobj.group(0)).hexdigest()
             extractions[digest] = matchobj.group(0)
             return "{gfm-extraction-%s}" % digest
         pattern = re.compile(r'<pre>.*?</pre>', re.MULTILINE | re.DOTALL)
--- a/kallithea/lib/middleware/https_fixup.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/middleware/https_fixup.py	Sat Aug 22 20:53:43 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/middleware/pygrack.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/middleware/pygrack.py	Sat Aug 22 20:53:43 2020 +0200
@@ -168,8 +168,9 @@
         if git_command in ['git-receive-pack']:
             # updating refs manually after each push.
             # Needed for pre-1.7.0.4 git clients using regular HTTP mode.
+            from dulwich.server import update_server_info
+
             from kallithea.lib.vcs import get_repo
-            from dulwich.server import update_server_info
             repo = get_repo(self.content_path)
             if repo:
                 update_server_info(repo._repo)
@@ -223,6 +224,6 @@
 
 
 def make_wsgi_app(repo_name, repo_root):
-    from dulwich.web import LimitedInputFilter, GunzipFilter
+    from dulwich.web import GunzipFilter, LimitedInputFilter
     app = GitDirectory(repo_root, repo_name)
     return GunzipFilter(LimitedInputFilter(app))
--- a/kallithea/lib/paster_commands/template.ini.mako	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/paster_commands/template.ini.mako	Sat Aug 22 20:53:43 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
-sqlalchemy.url = postgresql://user:pass@localhost/kallithea
-
-%elif database_engine == 'mysql':
-<%text>##</%text> MySQL
-sqlalchemy.url = mysql://user:pass@localhost/kallithea?charset=utf8
+%else:
+#sqlalchemy.url = sqlite:///%(here)s/kallithea.db?timeout=60
+%endif
+%if database_engine == 'postgres':
+sqlalchemy.url = postgresql://kallithea:password@localhost/kallithea
+%else:
+#sqlalchemy.url = postgresql://kallithea:password@localhost/kallithea
+%endif
+%if database_engine == 'mysql':
+sqlalchemy.url = mysql://kallithea:password@localhost/kallithea?charset=utf8mb4
+%else:
+#sqlalchemy.url = mysql://kallithea:password@localhost/kallithea?charset=utf8mb4
+%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/utils.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/utils.py	Sat Aug 22 20:53:43 2020 +0200
@@ -466,7 +466,7 @@
     enable_downloads = defs.get('repo_enable_downloads')
     private = defs.get('repo_private')
 
-    for name, repo in initial_repo_dict.items():
+    for name, repo in sorted(initial_repo_dict.items()):
         group = map_groups(name)
         db_repo = repo_model.get_by_repo_name(name)
         # found repo that is on filesystem not in Kallithea database
@@ -500,7 +500,7 @@
             new_repo.update_changeset_cache()
         elif install_git_hooks:
             if db_repo.repo_type == 'git':
-                ScmModel().install_git_hooks(db_repo.scm_instance, force_create=overwrite_git_hooks)
+                ScmModel().install_git_hooks(db_repo.scm_instance, force=overwrite_git_hooks)
 
     removed = []
     # remove from database those repositories that are not in the filesystem
--- a/kallithea/lib/utils2.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/utils2.py	Sat Aug 22 20:53:43 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
@@ -366,9 +331,8 @@
     :param repo:
     :param rev:
     """
-    from kallithea.lib.vcs.backends.base import BaseRepository
+    from kallithea.lib.vcs.backends.base import BaseRepository, EmptyChangeset
     from kallithea.lib.vcs.exceptions import RepositoryError
-    from kallithea.lib.vcs.backends.base import EmptyChangeset
     if not isinstance(repo, BaseRepository):
         raise Exception('You must pass an Repository '
                         'object as first argument got %s' % type(repo))
--- a/kallithea/lib/vcs/backends/hg/repository.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/vcs/backends/hg/repository.py	Sat Aug 22 20:53:43 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/lib/vcs/utils/helpers.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/lib/vcs/utils/helpers.py	Sat Aug 22 20:53:43 2020 +0200
@@ -112,8 +112,8 @@
     except ImportError:
         return code
     from pygments import highlight
-    from pygments.lexers import guess_lexer_for_filename, ClassNotFound
     from pygments.formatters import TerminalFormatter
+    from pygments.lexers import ClassNotFound, guess_lexer_for_filename
 
     try:
         lexer = guess_lexer_for_filename(name, code)
--- a/kallithea/model/comment.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/comment.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/db.py	Sat Aug 22 20:53:43 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
@@ -171,7 +167,6 @@
 
 _table_args_default_dict = {'extend_existing': True,
                             'mysql_engine': 'InnoDB',
-                            'mysql_charset': 'utf8',
                             'sqlite_autoincrement': True,
                            }
 
@@ -185,7 +180,7 @@
         'str': safe_bytes,
         'int': safe_int,
         'unicode': safe_str,
-        'bool': str2bool,
+        'bool': asbool,
         'list': functools.partial(aslist, sep=',')
     }
     DEFAULT_UPDATE_URL = ''
@@ -312,8 +307,10 @@
 
     @classmethod
     def get_server_info(cls):
+        import platform
+
         import pkg_resources
-        import platform
+
         from kallithea.lib.utils import check_git_version
         mods = [(p.project_name, p.version) for p in pkg_resources.working_set]
         info = {
@@ -600,7 +597,8 @@
 
         :param author:
         """
-        from kallithea.lib.helpers import email, author_name
+        from kallithea.lib.helpers import author_name, email
+
         # Valid email in the attribute passed, see if they're in the system
         _email = email(author)
         if _email:
@@ -1164,7 +1162,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 +1554,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 +1577,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 +1601,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 +1610,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/forms.py	Sat Aug 22 20:53:43 2020 +0200
@@ -396,7 +396,6 @@
 
 def DefaultPermissionsForm(repo_perms_choices, group_perms_choices,
                            user_group_perms_choices, create_choices,
-                           create_on_write_choices, repo_group_create_choices,
                            user_group_create_choices, fork_choices,
                            register_choices, extern_activate_choices):
     class _DefaultPermissionsForm(formencode.Schema):
@@ -411,9 +410,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/notification.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/permission.py	Sat Aug 22 20:53:43 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/repo.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/repo.py	Sat Aug 22 20:53:43 2020 +0200
@@ -109,7 +109,8 @@
 
     @classmethod
     def _render_datatable(cls, tmpl, *args, **kwargs):
-        from tg import tmpl_context as c, request, app_globals
+        from tg import app_globals, request
+        from tg import tmpl_context as c
         from tg.i18n import ugettext as _
 
         _tmpl_lookup = app_globals.mako_lookup
@@ -128,7 +129,9 @@
         admin: return data for action column.
         """
         _render = self._render_datatable
-        from tg import tmpl_context as c, request
+        from tg import request
+        from tg import tmpl_context as c
+
         from kallithea.model.scm import ScmModel
 
         def repo_lnk(name, rtype, rstate, private, fork_of):
@@ -666,7 +669,7 @@
         elif repo_type == 'git':
             repo = backend(repo_path, create=True, src_url=clone_uri, bare=True)
             # add kallithea hook into this repo
-            ScmModel().install_git_hooks(repo=repo)
+            ScmModel().install_git_hooks(repo)
         else:
             raise Exception('Not supported repo_type %s expected hg/git' % repo_type)
 
--- a/kallithea/model/repo_group.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/repo_group.py	Sat Aug 22 20:53:43 2020 +0200
@@ -189,8 +189,8 @@
     def _update_permissions(self, repo_group, perms_new=None,
                             perms_updates=None, recursive=None,
                             check_perms=True):
+        from kallithea.lib.auth import HasUserGroupPermissionLevel
         from kallithea.model.repo import RepoModel
-        from kallithea.lib.auth import HasUserGroupPermissionLevel
 
         if not perms_new:
             perms_new = []
--- a/kallithea/model/scm.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/scm.py	Sat Aug 22 20:53:43 2020 +0200
@@ -691,19 +691,19 @@
                 or sys.executable
                 or '/usr/bin/env python3')
 
-    def install_git_hooks(self, repo, force_create=False):
+    def install_git_hooks(self, repo, force=False):
         """
         Creates a kallithea hook inside a git repository
 
         :param repo: Instance of VCS repo
-        :param force_create: Create even if same name hook exists
+        :param force: Overwrite existing non-Kallithea hooks
         """
 
-        loc = os.path.join(repo.path, 'hooks')
+        hooks_path = os.path.join(repo.path, 'hooks')
         if not repo.bare:
-            loc = os.path.join(repo.path, '.git', 'hooks')
-        if not os.path.isdir(loc):
-            os.makedirs(loc)
+            hooks_path = os.path.join(repo.path, '.git', 'hooks')
+        if not os.path.isdir(hooks_path):
+            os.makedirs(hooks_path)
 
         tmpl_post = b"#!%s\n" % safe_bytes(self._get_git_hook_interpreter())
         tmpl_post += pkg_resources.resource_string(
@@ -715,40 +715,36 @@
         )
 
         for h_type, tmpl in [('pre', tmpl_pre), ('post', tmpl_post)]:
-            _hook_file = os.path.join(loc, '%s-receive' % h_type)
-            has_hook = False
+            hook_file = os.path.join(hooks_path, '%s-receive' % h_type)
+            other_hook = False
             log.debug('Installing git hook in repo %s', repo)
-            if os.path.exists(_hook_file):
+            if os.path.exists(hook_file):
                 # let's take a look at this hook, maybe it's kallithea ?
                 log.debug('hook exists, checking if it is from kallithea')
-                with open(_hook_file, 'rb') as f:
+                with open(hook_file, 'rb') as f:
                     data = f.read()
                     matches = re.search(br'^KALLITHEA_HOOK_VER\s*=\s*(.*)$', data, flags=re.MULTILINE)
                     if matches:
-                        try:
-                            ver = matches.groups()[0]
-                            log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver)
-                            has_hook = True
-                        except Exception:
-                            log.error(traceback.format_exc())
+                        ver = matches.groups()[0]
+                        log.debug('Found Kallithea hook - it has KALLITHEA_HOOK_VER %r', ver)
+                    else:
+                        log.debug('Found non-Kallithea hook at %s', hook_file)
+                        other_hook = True
+
+            if other_hook and not force:
+                log.warning('skipping overwriting hook file %s', hook_file)
             else:
-                # there is no hook in this dir, so we want to create one
-                has_hook = True
-
-            if has_hook or force_create:
                 log.debug('writing %s hook file !', h_type)
                 try:
-                    with open(_hook_file, 'wb') as f:
+                    with open(hook_file, 'wb') as f:
                         tmpl = tmpl.replace(b'_TMPL_', safe_bytes(kallithea.__version__))
                         f.write(tmpl)
-                    os.chmod(_hook_file, 0o755)
+                    os.chmod(hook_file, 0o755)
                 except IOError as e:
-                    log.error('error writing %s: %s', _hook_file, e)
-            else:
-                log.debug('skipping writing hook file')
+                    log.error('error writing hook %s: %s', hook_file, e)
 
 
-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 +755,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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/ssh_key.py	Sat Aug 22 20:53:43 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/user.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/user.py	Sat Aug 22 20:53:43 2020 +0200
@@ -59,8 +59,7 @@
         if not cur_user:
             cur_user = getattr(get_current_authuser(), 'username', None)
 
-        from kallithea.lib.hooks import log_create_user, \
-            check_allowed_create_user
+        from kallithea.lib.hooks import check_allowed_create_user, log_create_user
         _fd = form_data
         user_data = {
             'username': _fd['username'],
@@ -111,9 +110,8 @@
         if not cur_user:
             cur_user = getattr(get_current_authuser(), 'username', None)
 
-        from kallithea.lib.auth import get_crypt_password, check_password
-        from kallithea.lib.hooks import log_create_user, \
-            check_allowed_create_user
+        from kallithea.lib.auth import check_password, get_crypt_password
+        from kallithea.lib.hooks import check_allowed_create_user, log_create_user
         user_data = {
             'username': username, 'password': password,
             'email': email, 'firstname': firstname, 'lastname': lastname,
@@ -168,8 +166,8 @@
             raise
 
     def create_registration(self, form_data):
+        import kallithea.lib.helpers as h
         from kallithea.model.notification import NotificationModel
-        import kallithea.lib.helpers as h
 
         form_data['admin'] = False
         form_data['extern_type'] = User.DEFAULT_AUTH_TYPE
@@ -317,9 +315,9 @@
         allowing users to copy-paste or manually enter the token from the
         email.
         """
+        import kallithea.lib.helpers as h
         from kallithea.lib.celerylib import tasks
         from kallithea.model.notification import EmailNotificationModel
-        import kallithea.lib.helpers as h
 
         user_email = data['email']
         user = User.get_by_email(user_email)
@@ -386,8 +384,8 @@
         return expected_token == token
 
     def reset_password(self, user_email, new_passwd):
+        from kallithea.lib import auth
         from kallithea.lib.celerylib import tasks
-        from kallithea.lib import auth
         user = User.get_by_email(user_email)
         if user is not None:
             if not self.can_change_password(user):
--- a/kallithea/model/validators.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/model/validators.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/admin/permissions/permissions_globals.html	Sat Aug 22 20:53:43 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/changeset/changeset.html	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/changeset/changeset.html	Sat Aug 22 20:53:43 2020 +0200
@@ -47,8 +47,8 @@
                   <a href="${h.url('changeset_download_home',repo_name=c.repo_name,revision=c.changeset.raw_id,diff='download')}"
                      data-toggle="tooltip"
                      title="${_('Download diff')}"><i class="icon-floppy"></i></a>
-                  ${c.ignorews_url(request.GET)}
-                  ${c.context_url(request.GET)}
+                  ${h.ignore_whitespace_link(request.GET)}
+                  ${h.increase_context_link(request.GET)}
                 </div>
         </div>
         <div class="panel-body">
--- a/kallithea/templates/changeset/diff_block.html	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/changeset/diff_block.html	Sat Aug 22 20:53:43 2020 +0200
@@ -61,16 +61,16 @@
                       <i class="icon-file-code"></i></a>
                   <a href="${h.url('files_diff_2way_home',repo_name=cs_repo_name,f_path=cs_filename,diff2=cs_rev,diff1=a_rev,diff='diff',fulldiff=1)}" data-toggle="tooltip" title="${_('Show full side-by-side diff for this file')}">
                       <i class="icon-docs"></i></a>
-                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=cs_filename,diff2=cs_rev,diff1=a_rev,diff='raw')}" data-toggle="tooltip" title="${_('Raw diff')}">
+                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=cs_filename,diff2=cs_rev,diff1=a_rev,diff='raw')}" data-toggle="tooltip" title="${_('Raw diff for this file')}">
                       <i class="icon-diff"></i></a>
-                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=cs_filename,diff2=cs_rev,diff1=a_rev,diff='download')}" data-toggle="tooltip" title="${_('Download diff')}">
+                  <a href="${h.url('files_diff_home',repo_name=cs_repo_name,f_path=cs_filename,diff2=cs_rev,diff1=a_rev,diff='download')}" data-toggle="tooltip" title="${_('Download diff for this file')}">
                       <i class="icon-floppy"></i></a>
-                  ${c.ignorews_url(request.GET, url_fid)}
-                  ${c.context_url(request.GET, url_fid)}
+                  ${h.ignore_whitespace_link(request.GET, id_fid)}
+                  ${h.increase_context_link(request.GET, id_fid)}
                 </div>
                 <div class="pull-right">
                     ${_('Show inline comments')}
-                    ${h.checkbox('checkbox-show-inline-' + id_fid, checked="checked",class_="show-inline-comments",**{'data-id_for':id_fid})}
+                    ${h.checkbox('checkbox-show-inline-' + id_fid, checked="checked",class_="show-inline-comments",**{'data-for':id_fid})}
                 </div>
         </div>
         <div class="no-padding panel-body" data-f_path="${cs_filename}">
@@ -137,7 +137,7 @@
         if(target == null){
             target = this;
         }
-        var boxid = $(target).data('id_for');
+        var boxid = $(target).data('for');
         if(target.checked){
             $('#{0} .inline-comments'.format(boxid)).show();
         }else{
--- a/kallithea/templates/compare/compare_diff.html	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/compare/compare_diff.html	Sat Aug 22 20:53:43 2020 +0200
@@ -60,9 +60,8 @@
                 % else:
                     ${ungettext('%s file changed with %s insertions and %s deletions','%s files changed with %s insertions and %s deletions', len(c.file_diff_data)) % (len(c.file_diff_data),c.lines_added,c.lines_deleted)}:
                 %endif
-
-                ${c.ignorews_url(request.GET)}
-                ${c.context_url(request.GET)}
+                ${h.ignore_whitespace_link(request.GET)}
+                ${h.increase_context_link(request.GET)}
                 </h5>
                 <div class="cs_files">
                   %if not c.file_diff_data:
--- a/kallithea/templates/files/diff_2way.html	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/files/diff_2way.html	Sat Aug 22 20:53:43 2020 +0200
@@ -48,10 +48,10 @@
                           <i class="icon-docs"></i></a>
                       <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=c.node1.path,diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='raw')}"
                          data-toggle="tooltip"
-                         title="${_('Raw diff')}"><i class="icon-diff"></i></a>
+                         title="${_('Raw diff for this file')}"><i class="icon-diff"></i></a>
                       <a href="${h.url('files_diff_home',repo_name=c.repo_name,f_path=c.node1.path,diff2=c.cs2.raw_id,diff1=c.cs1.raw_id,diff='download')}"
                          data-toggle="tooltip"
-                         title="${_('Download diff')}"><i class="icon-floppy"></i></a>
+                         title="${_('Download diff for this file')}"><i class="icon-floppy"></i></a>
                       ${h.checkbox('ignorews', label=_('Ignore whitespace'))}
                       ${h.checkbox('edit_mode', label=_('Edit'))}
                     </div>
--- a/kallithea/templates/index_base.html	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/templates/index_base.html	Sat Aug 22 20:53:43 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/api/api_base.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/api/api_base.py	Sat Aug 22 20:53:43 2020 +0200
@@ -36,7 +36,7 @@
 from kallithea.model.user import UserModel
 from kallithea.model.user_group import UserGroupModel
 from kallithea.tests import base
-from kallithea.tests.fixture import Fixture
+from kallithea.tests.fixture import Fixture, raise_exception
 
 
 API_URL = '/_admin/api'
@@ -63,10 +63,6 @@
 jsonify = lambda obj: ext_json.loads(ext_json.dumps(obj))
 
 
-def crash(*args, **kwargs):
-    raise Exception('Total Crash !')
-
-
 def api_call(test_obj, params):
     response = test_obj.app.post(API_URL, content_type='application/json',
                                  params=params)
@@ -149,7 +145,7 @@
         assert 'trololo' == Optional.extract('trololo')
 
     def test_Optional_OAttr(self):
-        from kallithea.controllers.api.api import Optional, OAttr
+        from kallithea.controllers.api.api import OAttr, Optional
 
         option1 = Optional(OAttr('apiuser'))
         assert 'apiuser' == Optional.extract(option1)
@@ -349,7 +345,7 @@
         expected = {'added': [], 'removed': []}
         self._compare_ok(id_, expected, given=response.body)
 
-    @mock.patch.object(ScmModel, 'repo_scan', crash)
+    @mock.patch.object(ScmModel, 'repo_scan', raise_exception)
     def test_api_rescann_error(self):
         id_, params = _build_data(self.apikey, 'rescan_repos', )
         response = api_call(self, params)
@@ -439,7 +435,7 @@
         finally:
             fixture.destroy_user(usr.user_id)
 
-    @mock.patch.object(UserModel, 'create_or_update', crash)
+    @mock.patch.object(UserModel, 'create_or_update', raise_exception)
     def test_api_create_user_when_exception_happened(self):
 
         username = 'test_new_api_user'
@@ -473,7 +469,7 @@
         expected = ret
         self._compare_ok(id_, expected, given=response.body)
 
-    @mock.patch.object(UserModel, 'delete', crash)
+    @mock.patch.object(UserModel, 'delete', raise_exception)
     def test_api_delete_user_when_exception_happened(self):
         usr = UserModel().create_or_update(username='test_user',
                                            password='qweqwe',
@@ -561,7 +557,7 @@
         expected = 'editing default user is forbidden'
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(UserModel, 'update_user', crash)
+    @mock.patch.object(UserModel, 'update_user', raise_exception)
     def test_api_update_user_when_exception_happens(self):
         usr = User.get_by_username(base.TEST_USER_ADMIN_LOGIN)
         ret = jsonify(usr.get_api_data())
@@ -1020,7 +1016,7 @@
         self._compare_error(id_, expected, given=response.body)
         fixture.destroy_repo(repo_name)
 
-    @mock.patch.object(RepoModel, 'create', crash)
+    @mock.patch.object(RepoModel, 'create', raise_exception)
     def test_api_create_repo_exception_occurred(self):
         repo_name = 'api-repo'
         id_, params = _build_data(self.apikey, 'create_repo',
@@ -1140,7 +1136,7 @@
         finally:
             fixture.destroy_repo(repo_name)
 
-    @mock.patch.object(RepoModel, 'update', crash)
+    @mock.patch.object(RepoModel, 'update', raise_exception)
     def test_api_update_repo_exception_occurred(self):
         repo_name = 'api_update_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
@@ -1264,7 +1260,7 @@
         repo_name = 'api_delete_me'
         fixture.create_repo(repo_name, repo_type=self.REPO_TYPE)
         try:
-            with mock.patch.object(RepoModel, 'delete', crash):
+            with mock.patch.object(RepoModel, 'delete', raise_exception):
                 id_, params = _build_data(self.apikey, 'delete_repo',
                                           repoid=repo_name, )
                 response = api_call(self, params)
@@ -1412,7 +1408,7 @@
         expected = "repo `%s` already exist" % fork_name
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoModel, 'create_fork', crash)
+    @mock.patch.object(RepoModel, 'create_fork', raise_exception)
     def test_api_fork_repo_exception_occurred(self):
         fork_name = 'api-repo-fork'
         id_, params = _build_data(self.apikey, 'fork_repo',
@@ -1484,7 +1480,7 @@
         expected = "user group `%s` already exist" % TEST_USER_GROUP
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(UserGroupModel, 'create', crash)
+    @mock.patch.object(UserGroupModel, 'create', raise_exception)
     def test_api_get_user_group_exception_occurred(self):
         group_name = 'exception_happens'
         id_, params = _build_data(self.apikey, 'create_user_group',
@@ -1520,7 +1516,7 @@
                 gr_name = updates['group_name']
             fixture.destroy_user_group(gr_name)
 
-    @mock.patch.object(UserGroupModel, 'update', crash)
+    @mock.patch.object(UserGroupModel, 'update', raise_exception)
     def test_api_update_user_group_exception_occurred(self):
         gr_name = 'test_group'
         fixture.create_user_group(gr_name)
@@ -1559,7 +1555,7 @@
         expected = 'user group `%s` does not exist' % 'false-group'
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(UserGroupModel, 'add_user_to_group', crash)
+    @mock.patch.object(UserGroupModel, 'add_user_to_group', raise_exception)
     def test_api_add_user_to_user_group_exception_occurred(self):
         gr_name = 'test_group'
         fixture.create_user_group(gr_name)
@@ -1592,7 +1588,7 @@
         finally:
             fixture.destroy_user_group(gr_name)
 
-    @mock.patch.object(UserGroupModel, 'remove_user_from_group', crash)
+    @mock.patch.object(UserGroupModel, 'remove_user_from_group', raise_exception)
     def test_api_remove_user_from_user_group_exception_occurred(self):
         gr_name = 'test_group_3'
         gr = fixture.create_user_group(gr_name)
@@ -1651,7 +1647,7 @@
                                   usergroupid=gr_name)
 
         try:
-            with mock.patch.object(UserGroupModel, 'delete', crash):
+            with mock.patch.object(UserGroupModel, 'delete', raise_exception):
                 response = api_call(self, params)
                 expected = 'failed to delete user group ID:%s %s' % (gr_id, gr_name)
                 self._compare_error(id_, expected, given=response.body)
@@ -1693,7 +1689,7 @@
         expected = 'permission `%s` does not exist' % perm
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoModel, 'grant_user_permission', crash)
+    @mock.patch.object(RepoModel, 'grant_user_permission', raise_exception)
     def test_api_grant_user_permission_exception_when_adding(self):
         perm = 'repository.read'
         id_, params = _build_data(self.apikey,
@@ -1723,7 +1719,7 @@
         }
         self._compare_ok(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoModel, 'revoke_user_permission', crash)
+    @mock.patch.object(RepoModel, 'revoke_user_permission', raise_exception)
     def test_api_revoke_user_permission_exception_when_adding(self):
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission',
@@ -1771,7 +1767,7 @@
         expected = 'permission `%s` does not exist' % perm
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoModel, 'grant_user_group_permission', crash)
+    @mock.patch.object(RepoModel, 'grant_user_group_permission', raise_exception)
     def test_api_grant_user_group_permission_exception_when_adding(self):
         perm = 'repository.read'
         id_, params = _build_data(self.apikey,
@@ -1805,7 +1801,7 @@
         }
         self._compare_ok(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoModel, 'revoke_user_group_permission', crash)
+    @mock.patch.object(RepoModel, 'revoke_user_group_permission', raise_exception)
     def test_api_revoke_user_group_permission_exception_when_adding(self):
         id_, params = _build_data(self.apikey,
                                   'revoke_user_group_permission',
@@ -1907,7 +1903,7 @@
         expected = 'permission `%s` does not exist' % perm
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoGroupModel, 'grant_user_permission', crash)
+    @mock.patch.object(RepoGroupModel, 'grant_user_permission', raise_exception)
     def test_api_grant_user_permission_to_repo_group_exception_when_adding(self):
         perm = 'group.read'
         id_, params = _build_data(self.apikey,
@@ -1992,7 +1988,7 @@
             expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
             self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoGroupModel, 'revoke_user_permission', crash)
+    @mock.patch.object(RepoGroupModel, 'revoke_user_permission', raise_exception)
     def test_api_revoke_user_permission_from_repo_group_exception_when_adding(self):
         id_, params = _build_data(self.apikey,
                                   'revoke_user_permission_from_repo_group',
@@ -2096,7 +2092,7 @@
         expected = 'permission `%s` does not exist' % perm
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', crash)
+    @mock.patch.object(RepoGroupModel, 'grant_user_group_permission', raise_exception)
     def test_api_grant_user_group_permission_exception_when_adding_to_repo_group(self):
         perm = 'group.read'
         id_, params = _build_data(self.apikey,
@@ -2180,7 +2176,7 @@
             expected = 'repository group `%s` does not exist' % TEST_REPO_GROUP
             self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(RepoGroupModel, 'revoke_user_group_permission', crash)
+    @mock.patch.object(RepoGroupModel, 'revoke_user_group_permission', raise_exception)
     def test_api_revoke_user_group_permission_from_repo_group_exception_when_adding(self):
         id_, params = _build_data(self.apikey, 'revoke_user_group_permission_from_repo_group',
                                   repogroupid=TEST_REPO_GROUP,
@@ -2301,7 +2297,7 @@
         }
         self._compare_ok(id_, expected, given=response.body)
 
-    @mock.patch.object(GistModel, 'create', crash)
+    @mock.patch.object(GistModel, 'create', raise_exception)
     def test_api_create_gist_exception_occurred(self):
         id_, params = _build_data(self.apikey_regular, 'create_gist',
                                   files={})
@@ -2333,7 +2329,7 @@
         expected = 'gist `%s` does not exist' % (gist_id,)
         self._compare_error(id_, expected, given=response.body)
 
-    @mock.patch.object(GistModel, 'delete', crash)
+    @mock.patch.object(GistModel, 'delete', raise_exception)
     def test_api_delete_gist_exception_occurred(self):
         gist_id = fixture.create_gist().gist_access_id
         id_, params = _build_data(self.apikey, 'delete_gist',
--- a/kallithea/tests/conftest.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/conftest.py	Sat Aug 22 20:53:43 2020 +0200
@@ -59,8 +59,12 @@
             'formatter': 'color_formatter_sql',
         },
     }
-    if os.environ.get('TEST_DB'):
-        ini_settings['[app:main]']['sqlalchemy.url'] = os.environ.get('TEST_DB')
+    create_database = os.environ.get('TEST_DB')  # TODO: rename to 'CREATE_TEST_DB'
+    if create_database:
+        ini_settings['[app:main]']['sqlalchemy.url'] = create_database
+    reuse_database = os.environ.get('REUSE_TEST_DB')
+    if reuse_database:
+        ini_settings['[app:main]']['sqlalchemy.url'] = reuse_database
 
     test_ini_file = os.path.join(TESTS_TMP_PATH, 'test.ini')
     inifile.create(test_ini_file, None, ini_settings)
@@ -70,7 +74,7 @@
 
     # set KALLITHEA_NO_TMP_PATH=1 to disable re-creating the database and test repos
     if not int(os.environ.get('KALLITHEA_NO_TMP_PATH', 0)):
-        create_test_env(TESTS_TMP_PATH, context.config())
+        create_test_env(TESTS_TMP_PATH, context.config(), reuse_database=bool(reuse_database))
 
     # set KALLITHEA_WHOOSH_TEST_DISABLE=1 to disable whoosh index during tests
     if not int(os.environ.get('KALLITHEA_WHOOSH_TEST_DISABLE', 0)):
--- a/kallithea/tests/fixture.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/fixture.py	Sat Aug 22 20:53:43 2020 +0200
@@ -50,8 +50,8 @@
 FIXTURES = os.path.join(dirname(dirname(os.path.abspath(__file__))), 'tests', 'fixtures')
 
 
-def error_function(*args, **kwargs):
-    raise Exception('Total Crash !')
+def raise_exception(*args, **kwargs):
+    raise Exception('raise_exception raised exception')
 
 
 class Fixture(object):
@@ -349,7 +349,7 @@
 # Global test environment setup
 #==============================================================================
 
-def create_test_env(repos_test_path, config):
+def create_test_env(repos_test_path, config, reuse_database):
     """
     Makes a fresh database and
     install test repository into tmp dir
@@ -366,7 +366,7 @@
 
     dbmanage = DbManage(dbconf=dbconf, root=config['here'],
                         tests=True)
-    dbmanage.create_tables(override=True)
+    dbmanage.create_tables(reuse_database=reuse_database)
     # for tests dynamically set new root paths based on generated content
     dbmanage.create_settings(dbmanage.prompt_repo_root_path(repos_test_path))
     dbmanage.create_default_user()
--- a/kallithea/tests/functional/test_admin_repos.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/functional/test_admin_repos.py	Sat Aug 22 20:53:43 2020 +0200
@@ -14,7 +14,7 @@
 from kallithea.model.repo_group import RepoGroupModel
 from kallithea.model.user import UserModel
 from kallithea.tests import base
-from kallithea.tests.fixture import Fixture, error_function
+from kallithea.tests.fixture import Fixture, raise_exception
 
 
 fixture = Fixture()
@@ -605,7 +605,7 @@
         RepoModel().delete(repo_name)
         Session().commit()
 
-    @mock.patch.object(RepoModel, '_create_filesystem_repo', error_function)
+    @mock.patch.object(RepoModel, '_create_filesystem_repo', raise_exception)
     def test_create_repo_when_filesystem_op_fails(self):
         self.log_user()
         repo_name = self.NEW_REPO
--- a/kallithea/tests/models/test_diff_parsers.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/models/test_diff_parsers.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/models/test_dump_html_mails.ref.html	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/models/test_notifications.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/models/test_permissions.py	Sat Aug 22 20:53:43 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	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/other/test_libs.py	Sat Aug 22 20:53:43 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
@@ -158,8 +156,9 @@
         (dict(years= -3, months= -2), '3 years and 2 months ago'),
     ])
     def test_age(self, age_args, expected):
+        from dateutil import relativedelta
+
         from kallithea.lib.utils2 import age
-        from dateutil import relativedelta
         with test_context(self.app):
             n = datetime.datetime(year=2012, month=5, day=17)
             delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
@@ -183,8 +182,9 @@
         (dict(years= -4, months= -8), '5 years ago'),
     ])
     def test_age_short(self, age_args, expected):
+        from dateutil import relativedelta
+
         from kallithea.lib.utils2 import age
-        from dateutil import relativedelta
         with test_context(self.app):
             n = datetime.datetime(year=2012, month=5, day=17)
             delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
@@ -202,8 +202,9 @@
         (dict(years=1, months=1), 'in 1 year and 1 month')
     ])
     def test_age_in_future(self, age_args, expected):
+        from dateutil import relativedelta
+
         from kallithea.lib.utils2 import age
-        from dateutil import relativedelta
         with test_context(self.app):
             n = datetime.datetime(year=2012, month=5, day=17)
             delt = lambda *args, **kwargs: relativedelta.relativedelta(*args, **kwargs)
@@ -299,6 +300,7 @@
         :param text:
         """
         import re
+
         # quickly change expected url[] into a link
         url_pattern = re.compile(r'(?:url\[)(.+?)(?:\])')
 
@@ -572,11 +574,11 @@
         ('http://www.example.org/kallithea/repos/', 'abc/xyz/', 'http://www.example.org/kallithea/repos/abc/xyz/'),
     ])
     def test_canonical_url(self, canonical, test, expected):
-        from kallithea.lib.helpers import canonical_url
+        # setup url(), used by canonical_url
+        import routes
         from tg import request
 
-        # setup url(), used by canonical_url
-        import routes
+        from kallithea.lib.helpers import canonical_url
         m = routes.Mapper()
         m.connect('about', '/about-page')
         url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
@@ -596,11 +598,12 @@
         ('http://www.example.org/kallithea/repos/', 'www.example.org'),
     ])
     def test_canonical_hostname(self, canonical, expected):
-        from kallithea.lib.helpers import canonical_hostname
+        import routes
         from tg import request
 
+        from kallithea.lib.helpers import canonical_hostname
+
         # setup url(), used by canonical_hostname
-        import routes
         m = routes.Mapper()
         url = routes.URLGenerator(m, {'HTTP_HOST': 'http_host.example.org'})
 
--- a/kallithea/tests/other/test_vcs_operations.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/other/test_vcs_operations.py	Sat Aug 22 20:53:43 2020 +0200
@@ -308,7 +308,7 @@
         if vt.repo_type == 'git':
             assert 'not found' in stderr or 'abort: Access to %r denied' % 'trololo' in stderr
         elif vt.repo_type == 'hg':
-            assert 'HTTP Error 404: Not Found' in stderr or 'abort: no suitable response from remote hg' in stderr and 'remote: abort: Access to %r denied' % 'trololo' in stdout
+            assert 'HTTP Error 404: Not Found' in stderr or 'abort: no suitable response from remote hg' in stderr and 'remote: abort: Access to %r denied' % 'trololo' in stdout + stderr
 
     @parametrize_vcs_test
     def test_push_new_repo(self, webserver, vt):
@@ -358,6 +358,7 @@
         # <UserLog('id:new_git_XXX:user_created_repo')>
         # <UserLog('id:new_git_XXX:pull')>
         # <UserLog('id:new_git_XXX:push:aed9d4c1732a1927da3be42c47eb9afdc200d427,d38b083a07af10a9f44193486959a96a23db78da,4841ff9a2b385bec995f4679ef649adb3f437622')>
+        Session.close()  # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
         action_parts = [ul.action.split(':', 1) for ul in UserLog.query().order_by(UserLog.user_log_id)]
         assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == ([
             ('started_following_repo', 0),
@@ -389,6 +390,7 @@
             assert 'Repository size' in stdout
             assert 'Last revision is now' in stdout
 
+        Session.close()  # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
         action_parts = [ul.action.split(':', 1) for ul in UserLog.query().order_by(UserLog.user_log_id)]
         assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
             [('pull', 0), ('push', 3)]
@@ -403,6 +405,7 @@
 
         clone_url = vt.repo_url_param(webserver, vt.repo_name)
         stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url)
+        Session.close()  # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
 
         if vt.repo_type == 'git':
             assert 'FETCH_HEAD' in stderr
@@ -426,7 +429,7 @@
             if vt.repo_type == 'git':
                 assert "abort: Access to './%s' denied" % vt.repo_name in stderr
             else:
-                assert "abort: Access to './%s' denied" % vt.repo_name in stdout
+                assert "abort: Access to './%s' denied" % vt.repo_name in stdout + stderr
 
         stdout, stderr = Command(dest_dir).execute(vt.repo_type, 'pull', clone_url.replace('/' + vt.repo_name, '/%s/' % vt.repo_name), ignoreReturnCode=True)
         if vt.repo_type == 'git':
@@ -448,11 +451,10 @@
 
         stdout, stderr = _add_files_and_push(webserver, vt, dest_dir, files_no=1, clone_url=clone_url)
 
-        Session().commit()  # expire test session to make sure SA fetch new Repository instances after last_changeset has been updated server side hook in other process
-
         if vt.repo_type == 'git':
             _check_proper_git_push(stdout, stderr)
 
+        Session.close()  # expire session to make sure SA fetches new Repository instances after last_changeset has been updated by server side hook in another process
         post_cached_tip = [repo.get_api_data()['last_changeset']['short_id'] for repo in Repository.query().filter(Repository.repo_name == testfork[vt.repo_type])]
         assert pre_cached_tip != post_cached_tip
 
@@ -487,6 +489,7 @@
         elif vt.repo_type == 'hg':
             assert 'abort: HTTP Error 403: Forbidden' in stderr or 'abort: push failed on remote' in stderr and 'remote: Push access to %r denied' % str(vt.repo_name) in stdout
 
+        Session.close()  # make sure SA fetches all new log entries (apparently only needed for MariaDB/MySQL ...)
         action_parts = [ul.action.split(':', 1) for ul in UserLog.query().order_by(UserLog.user_log_id)]
         assert [(t[0], (t[1].count(',') + 1) if len(t) == 2 else 0) for t in action_parts] == \
             [('pull', 0)]
@@ -522,7 +525,7 @@
                 # The message apparently changed in Git 1.8.3, so match it loosely.
                 assert re.search(r'\b403\b', stderr) or 'abort: User test_admin from 127.0.0.127 cannot be authorized' in stderr
             elif vt.repo_type == 'hg':
-                assert 'abort: HTTP Error 403: Forbidden' in stderr or 'remote: abort: User test_admin from 127.0.0.127 cannot be authorized' in stdout
+                assert 'abort: HTTP Error 403: Forbidden' in stderr or 'remote: abort: User test_admin from 127.0.0.127 cannot be authorized' in stdout + stderr
         finally:
             # release IP restrictions
             for ip in UserIpMap.query():
--- a/kallithea/tests/scripts/manual_test_concurrency.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/scripts/manual_test_concurrency.py	Sat Aug 22 20:53:43 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/kallithea/tests/vcs/test_git.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/vcs/test_git.py	Sat Aug 22 20:53:43 2020 +0200
@@ -794,7 +794,7 @@
             if os.path.exists(hook_path):
                 os.remove(hook_path)
 
-        ScmModel().install_git_hooks(repo=self.repo)
+        ScmModel().install_git_hooks(self.repo)
 
         for hook, hook_path in self.kallithea_hooks.items():
             assert os.path.exists(hook_path)
@@ -808,7 +808,7 @@
             with open(hook_path, "w") as f:
                 f.write("KALLITHEA_HOOK_VER=0.0.0\nJUST_BOGUS")
 
-        ScmModel().install_git_hooks(repo=self.repo)
+        ScmModel().install_git_hooks(self.repo)
 
         for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
@@ -823,7 +823,7 @@
             with open(hook_path, "w") as f:
                 f.write("#!/bin/bash\n#CUSTOM_HOOK")
 
-        ScmModel().install_git_hooks(repo=self.repo)
+        ScmModel().install_git_hooks(self.repo)
 
         for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
@@ -838,7 +838,7 @@
             with open(hook_path, "w") as f:
                 f.write("#!/bin/bash\n#CUSTOM_HOOK")
 
-        ScmModel().install_git_hooks(repo=self.repo, force_create=True)
+        ScmModel().install_git_hooks(self.repo, force=True)
 
         for hook, hook_path in self.kallithea_hooks.items():
             with open(hook_path) as f:
--- a/kallithea/tests/vcs/test_workdirs.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/kallithea/tests/vcs/test_workdirs.py	Sat Aug 22 20:53:43 2020 +0200
@@ -68,6 +68,7 @@
 
     def test_checkout_branch(self):
         from kallithea.lib.vcs.exceptions import BranchDoesNotExistError
+
         # first, 'foobranch' does not exist.
         with pytest.raises(BranchDoesNotExistError):
             self.repo.workdir.checkout_branch(branch='foobranch')
--- a/scripts/i18n	Sun Jul 26 00:03:12 2020 +0200
+++ b/scripts/i18n	Sat Aug 22 20:53:43 2020 +0200
@@ -19,7 +19,6 @@
 import sys
 
 import click
-
 import i18n_utils
 
 
@@ -90,11 +89,8 @@
 
     and then invoke merge/rebase/graft with the additional argument '--tool i18n'.
     """
-    from mercurial import (
-        context,
-        simplemerge,
-        ui as uimod,
-    )
+    from mercurial import context, simplemerge
+    from mercurial import ui as uimod
 
     print('i18n normalized-merge: normalizing and merging %s' % output)
 
--- a/setup.py	Sun Jul 26 00:03:12 2020 +0200
+++ b/setup.py	Sat Aug 22 20:53:43 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",
@@ -63,9 +63,9 @@
     "URLObject >= 2.3.4, < 2.5",
     "Routes >= 2.0, < 2.5",
     "dulwich >= 0.19.0, < 0.20",
-    "mercurial >= 5.2, < 5.5",
+    "mercurial >= 5.2, < 5.6",
     "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
     """,
 )